From 5b2eee84be77210a3c683a4baf452f2210a2e03b Mon Sep 17 00:00:00 2001 From: Jonny Nguyen Date: Fri, 26 Jun 2026 14:02:44 -0700 Subject: [PATCH] Add query cache utilities for campaign data invalidation Introduce new functions in query-cache.ts to handle cache invalidation for campaign-related queries. Update CampaignDetailPage, CampaignsPage, and CampaignWizardPage to utilize these functions for improved cache management upon successful mutations, ensuring the UI reflects the latest campaign data. --- apps/web/src/lib/query-cache.ts | 21 +++++++++++++++++++++ apps/web/src/pages/CampaignDetailPage.tsx | 21 +++++++++------------ apps/web/src/pages/CampaignWizardPage.tsx | 5 +++-- apps/web/src/pages/CampaignsPage.tsx | 5 +++-- 4 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/lib/query-cache.ts diff --git a/apps/web/src/lib/query-cache.ts b/apps/web/src/lib/query-cache.ts new file mode 100644 index 0000000..cc2d100 --- /dev/null +++ b/apps/web/src/lib/query-cache.ts @@ -0,0 +1,21 @@ +import type { QueryClient } from '@tanstack/react-query'; + +/** Invalidate list + dashboard after any campaign mutation. */ +export function invalidateCampaignListCaches(queryClient: QueryClient): Promise { + return Promise.all([ + queryClient.invalidateQueries({ queryKey: ['campaigns'] }), + queryClient.invalidateQueries({ queryKey: ['workspace', 'overview'] }), + ]).then(() => undefined); +} + +/** Invalidate detail, stats, list, and dashboard for one campaign. */ +export function invalidateCampaignCaches( + queryClient: QueryClient, + campaignId: string, +): Promise { + return Promise.all([ + queryClient.invalidateQueries({ queryKey: ['campaign', campaignId] }), + queryClient.invalidateQueries({ queryKey: ['campaign', campaignId, 'stats'] }), + invalidateCampaignListCaches(queryClient), + ]).then(() => undefined); +} diff --git a/apps/web/src/pages/CampaignDetailPage.tsx b/apps/web/src/pages/CampaignDetailPage.tsx index d8f46ff..755229d 100644 --- a/apps/web/src/pages/CampaignDetailPage.tsx +++ b/apps/web/src/pages/CampaignDetailPage.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { Send, MessageSquare, Shield, Inbox, Pause, Play, Loader2, Trash2 } from 'lucide-react'; import { apiGet, apiPatch, apiPost, apiDelete } from '../lib/api'; +import { invalidateCampaignCaches, invalidateCampaignListCaches } from '../lib/query-cache'; import type { Campaign, CampaignSatellite, @@ -120,8 +121,8 @@ export function CampaignDetailPage() { const saveSettingsMutation = useMutation({ mutationFn: (payload: typeof settingsForm) => apiPatch(`/api/campaigns/${id}`, payload), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['campaign', id] }); + onSuccess: async () => { + if (id) await invalidateCampaignCaches(queryClient, id); setSettingsMessage('Campaign settings saved'); setSettingsError(''); }, @@ -130,19 +131,15 @@ export function CampaignDetailPage() { const pauseMutation = useMutation({ mutationFn: () => apiPost(`/api/campaigns/${id}/pause`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['campaign', id] }); - queryClient.invalidateQueries({ queryKey: ['campaign', id, 'stats'] }); - queryClient.invalidateQueries({ queryKey: ['workspace', 'overview'] }); + onSuccess: async () => { + if (id) await invalidateCampaignCaches(queryClient, id); }, }); const resumeMutation = useMutation({ mutationFn: () => apiPost(`/api/campaigns/${id}/resume`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['campaign', id] }); - queryClient.invalidateQueries({ queryKey: ['campaign', id, 'stats'] }); - queryClient.invalidateQueries({ queryKey: ['workspace', 'overview'] }); + onSuccess: async () => { + if (id) await invalidateCampaignCaches(queryClient, id); }, }); @@ -165,8 +162,8 @@ export function CampaignDetailPage() { const deleteMutation = useMutation({ mutationFn: () => apiDelete(`/api/campaigns/${id}`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['campaigns'] }); + onSuccess: async () => { + await invalidateCampaignListCaches(queryClient); queryClient.invalidateQueries({ queryKey: ['mailboxes'] }); navigate('/campaigns'); }, diff --git a/apps/web/src/pages/CampaignWizardPage.tsx b/apps/web/src/pages/CampaignWizardPage.tsx index 70813ce..7c86ffe 100644 --- a/apps/web/src/pages/CampaignWizardPage.tsx +++ b/apps/web/src/pages/CampaignWizardPage.tsx @@ -5,6 +5,7 @@ import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; import type { CampaignRecipe } from '@warmbox/shared'; import { PROVIDER_LABELS } from '@warmbox/shared'; import { apiDelete, apiGet, apiPost } from '../lib/api'; +import { invalidateCampaignListCaches } from '../lib/query-cache'; import type { Campaign, CreateCampaignInput, Mailbox, RecipePreviewDay } from '../lib/types'; import { PageHeader } from '../components/PageHeader'; import { Button } from '../components/ui/Button'; @@ -99,8 +100,8 @@ export function CampaignWizardPage() { } return campaign; }, - onSuccess: (campaign) => { - queryClient.invalidateQueries({ queryKey: ['campaigns'] }); + onSuccess: async (campaign) => { + await invalidateCampaignListCaches(queryClient); queryClient.invalidateQueries({ queryKey: ['mailboxes'] }); navigate(`/campaigns/${campaign.id}`); }, diff --git a/apps/web/src/pages/CampaignsPage.tsx b/apps/web/src/pages/CampaignsPage.tsx index e16d3d5..142bd7f 100644 --- a/apps/web/src/pages/CampaignsPage.tsx +++ b/apps/web/src/pages/CampaignsPage.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; import { Plus, Megaphone, Trash2 } from 'lucide-react'; import { apiDelete, apiGet } from '../lib/api'; +import { invalidateCampaignListCaches } from '../lib/query-cache'; import type { Campaign } from '../lib/types'; import { PageHeader } from '../components/PageHeader'; import { Button } from '../components/ui/Button'; @@ -22,8 +23,8 @@ export function CampaignsPage() { const deleteMutation = useMutation({ mutationFn: (id: string) => apiDelete(`/api/campaigns/${id}`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['campaigns'] }); + onSuccess: async () => { + await invalidateCampaignListCaches(queryClient); queryClient.invalidateQueries({ queryKey: ['mailboxes'] }); }, });