diff --git a/templates/analytics/app/components/layout/Sidebar.tsx b/templates/analytics/app/components/layout/Sidebar.tsx index 04990c503..672a2c4b7 100644 --- a/templates/analytics/app/components/layout/Sidebar.tsx +++ b/templates/analytics/app/components/layout/Sidebar.tsx @@ -4,7 +4,12 @@ import { useTheme } from "next-themes"; import { cn, shortcutModifierLabel } from "@/lib/utils"; import { useAuth } from "@/components/auth/AuthProvider"; import { toast } from "sonner"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + useQuery, + useQueryClient, + type QueryClient, + type QueryKey, +} from "@tanstack/react-query"; import { IconChartBar, IconLogout, @@ -89,11 +94,16 @@ import { NewDashboardDialog } from "./NewDashboardDialog"; import { NewAnalysisDialog } from "./NewAnalysisDialog"; import { useUserPref } from "@/hooks/use-user-pref"; import { - useAllDashboardViews, + useDashboardViews, useDeleteDashboardView, type DashboardView, } from "@/hooks/use-dashboard-views"; import { usePopularity, popularityOf } from "@/lib/item-popularity"; +import { + analysisDetailPrefetchKey, + sqlDashboardPrefetchKey, + type PrefetchSnapshot, +} from "@/lib/prefetch-keys"; const SIDEBAR_PREVIEW_COUNT = 5; const DASHBOARD_SORT_MODE_KEY = "dashboard-sort-mode"; @@ -258,6 +268,7 @@ function SortableRow({ onDelete, onRename, onArchive, + onPrefetch, archived, children, }: { @@ -274,6 +285,7 @@ function SortableRow({ * action and Delete becomes a confirm-gated "Delete permanently". When * omitted, Delete fires immediately with no confirm (analyses behavior). */ onArchive?: (action: "archive" | "restore") => Promise | void; + onPrefetch?: () => void; archived?: boolean; children?: React.ReactNode; }) { @@ -390,6 +402,9 @@ function SortableRow({ {name} @@ -537,6 +552,7 @@ function SortableDashboardItem({ onDelete, onRename, onArchive, + onPrefetch, views, }: { d: SidebarDashboard; @@ -550,6 +566,7 @@ function SortableDashboardItem({ d: SidebarDashboard, action: "archive" | "restore", ) => Promise; + onPrefetch?: (d: SidebarDashboard) => void; views?: DashboardView[]; }) { const href = `/adhoc/${d.id}`; @@ -601,6 +618,7 @@ function SortableDashboardItem({ onDelete={() => onDelete(d)} onRename={(name) => onRename(d, name)} onArchive={onArchive ? (action) => onArchive(d, action) : undefined} + onPrefetch={() => onPrefetch?.(d)} archived={!!d.archivedAt} > {isActive && allSubviews.length > 0 && ( @@ -923,6 +941,82 @@ async function fetchSidebarAnalyses(): Promise<{ id: string; name: string }[]> { })); } +type PrefetchedSqlDashboard = { + id: string; + config: { + name: string; + description?: string; + filters?: unknown; + variables?: unknown; + columns?: number; + panels: unknown[]; + }; + archivedAt: string | null; +}; + +async function fetchSqlDashboardForPrefetch( + id: string, +): Promise { + const token = await getIdToken(); + const res = await fetch(appApiPath(`/api/sql-dashboards/${id}`), { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + if (res.status === 404) return null; + if (!res.ok) { + throw new Error(`Dashboard prefetch failed (${res.status})`); + } + let data: any; + try { + data = await res.json(); + } catch { + throw new Error("Dashboard prefetch returned invalid JSON"); + } + return { + id, + config: { + name: + typeof data.name === "string" && data.name.trim().length > 0 + ? data.name + : "Untitled Dashboard", + description: data.description, + filters: data.filters, + variables: data.variables, + columns: typeof data.columns === "number" ? data.columns : undefined, + panels: Array.isArray(data.panels) ? data.panels : [], + }, + archivedAt: typeof data.archivedAt === "string" ? data.archivedAt : null, + }; +} + +async function fetchAnalysisDetailForPrefetch(id: string): Promise { + const token = await getIdToken(); + const res = await fetch(appApiPath(`/api/analyses/${id}`), { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + if (res.status === 404) return null; + if (!res.ok) { + throw new Error(`Analysis prefetch failed (${res.status})`); + } + try { + return await res.json(); + } catch { + throw new Error("Analysis prefetch returned invalid JSON"); + } +} + +function getQuerySnapshots(queryClient: QueryClient, queryKey: QueryKey) { + return queryClient.getQueriesData({ queryKey }); +} + +function restoreQuerySnapshots( + queryClient: QueryClient, + snapshots: Array<[QueryKey, T | undefined]>, +) { + for (const [key, data] of snapshots) { + queryClient.setQueryData(key, data); + } +} + function persistThemePreference(theme: "light" | "dark") { fetch(appApiPath("/api/theme"), { method: "POST", @@ -1103,16 +1197,61 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { [sortedAnalyses, analysesShowAll], ); - // Fetch views for all dashboards (for sidebar sub-items) - const allDashboardIds = useMemo(() => { - const ids = dashboards.map((d) => d.id); - for (const sd of sqlDashboards) { - if (!ids.includes(sd.id)) ids.push(sd.id); - } - return ids; - }, [sqlDashboards]); + const activeDashboardId = useMemo(() => { + const match = location.pathname.match(/^\/adhoc\/([^/]+)/); + if (!match?.[1]) return null; + return new URLSearchParams(location.search).get("id") || match[1]; + }, [location.pathname, location.search]); + + // Only the active dashboard can display saved views in the sidebar, so avoid + // issuing one request per dashboard on every sidebar mount. + const { views: activeDashboardViews = [] } = useDashboardViews( + activeDashboardId ?? undefined, + ); + const allViewsMap = useMemo>( + () => + activeDashboardId ? { [activeDashboardId]: activeDashboardViews } : {}, + [activeDashboardId, activeDashboardViews], + ); - const { data: allViewsMap = {} } = useAllDashboardViews(allDashboardIds); + const prefetchDashboard = useCallback( + (d: SidebarDashboard) => { + if (d.source !== "sql") return; + const queryKey = sqlDashboardPrefetchKey(d.id); + const cached = + queryClient.getQueryData< + PrefetchSnapshot + >(queryKey); + void import("@/pages/adhoc/sql-dashboard"); + void queryClient.prefetchQuery({ + queryKey, + queryFn: async () => ({ + data: await fetchSqlDashboardForPrefetch(d.id), + syncVersion: dashboardsSync, + }), + staleTime: cached?.syncVersion === dashboardsSync ? 30_000 : 0, + }); + }, + [dashboardsSync, queryClient], + ); + + const prefetchAnalysis = useCallback( + (id: string) => { + const queryKey = analysisDetailPrefetchKey(id); + const cached = + queryClient.getQueryData>(queryKey); + void import("@/pages/analyses/AnalysisDetail"); + void queryClient.prefetchQuery({ + queryKey, + queryFn: async () => ({ + data: await fetchAnalysisDetailForPrefetch(id), + syncVersion: analysesSync, + }), + staleTime: cached?.syncVersion === analysesSync ? 30_000 : 0, + }); + }, + [analysesSync, queryClient], + ); const visibleDashboards = useMemo(() => { const staticItems: SidebarDashboard[] = dashboards @@ -1175,15 +1314,21 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { // the prior values so we can roll back on failure. const activeKey = ["sql-dashboards-sidebar"] as const; const archivedKey = ["sql-dashboards-archived-sidebar"] as const; - const prevActive = - queryClient.getQueryData(activeKey); - const prevArchived = - queryClient.getQueryData(archivedKey); - queryClient.setQueryData(activeKey, (old) => - (old ?? []).filter((item) => item.id !== d.id), + const prevActive = getQuerySnapshots( + queryClient, + activeKey, + ); + const prevArchived = getQuerySnapshots( + queryClient, + archivedKey, + ); + queryClient.setQueriesData( + { queryKey: activeKey }, + (old) => (old ?? []).filter((item) => item.id !== d.id), ); - queryClient.setQueryData(archivedKey, (old) => - (old ?? []).filter((item) => item.id !== d.id), + queryClient.setQueriesData( + { queryKey: archivedKey }, + (old) => (old ?? []).filter((item) => item.id !== d.id), ); try { const token = await getIdToken(); @@ -1194,11 +1339,12 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { if (!res.ok) { throw new Error(`Delete failed: ${res.status}`); } + queryClient.removeQueries({ queryKey: sqlDashboardPrefetchKey(d.id) }); queryClient.invalidateQueries({ queryKey: activeKey }); queryClient.invalidateQueries({ queryKey: archivedKey }); } catch (err) { - if (prevActive) queryClient.setQueryData(activeKey, prevActive); - if (prevArchived) queryClient.setQueryData(archivedKey, prevArchived); + restoreQuerySnapshots(queryClient, prevActive); + restoreQuerySnapshots(queryClient, prevArchived); throw err; } }, @@ -1218,27 +1364,39 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { } const activeKey = ["sql-dashboards-sidebar"] as const; const archivedKey = ["sql-dashboards-archived-sidebar"] as const; - const prevActive = - queryClient.getQueryData(activeKey); - const prevArchived = - queryClient.getQueryData(archivedKey); + const prevActive = getQuerySnapshots( + queryClient, + activeKey, + ); + const prevArchived = getQuerySnapshots( + queryClient, + archivedKey, + ); // Optimistic move between the two lists. if (action === "archive") { - queryClient.setQueryData(activeKey, (old) => - (old ?? []).filter((item) => item.id !== d.id), + queryClient.setQueriesData( + { queryKey: activeKey }, + (old) => (old ?? []).filter((item) => item.id !== d.id), + ); + queryClient.setQueriesData( + { queryKey: archivedKey }, + (old) => [ + ...(old ?? []), + { id: d.id, name: d.name, archivedAt: new Date().toISOString() }, + ], ); - queryClient.setQueryData(archivedKey, (old) => [ - ...(old ?? []), - { id: d.id, name: d.name, archivedAt: new Date().toISOString() }, - ]); } else { - queryClient.setQueryData(archivedKey, (old) => - (old ?? []).filter((item) => item.id !== d.id), + queryClient.setQueriesData( + { queryKey: archivedKey }, + (old) => (old ?? []).filter((item) => item.id !== d.id), + ); + queryClient.setQueriesData( + { queryKey: activeKey }, + (old) => [ + ...(old ?? []), + { id: d.id, name: d.name, archivedAt: null }, + ], ); - queryClient.setQueryData(activeKey, (old) => [ - ...(old ?? []), - { id: d.id, name: d.name, archivedAt: null }, - ]); } try { const token = await getIdToken(); @@ -1251,6 +1409,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); if (!res.ok) throw new Error(`${action} failed: ${res.status}`); + queryClient.removeQueries({ queryKey: sqlDashboardPrefetchKey(d.id) }); queryClient.invalidateQueries({ queryKey: activeKey }); queryClient.invalidateQueries({ queryKey: archivedKey }); toast.success( @@ -1259,8 +1418,8 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { : `Restored "${d.name}"`, ); } catch (err) { - if (prevActive) queryClient.setQueryData(activeKey, prevActive); - if (prevArchived) queryClient.setQueryData(archivedKey, prevArchived); + restoreQuerySnapshots(queryClient, prevActive); + restoreQuerySnapshots(queryClient, prevArchived); throw err; } }, @@ -1282,23 +1441,24 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { } const queryKey = ["sql-dashboards-sidebar"] as const; - const prev = - queryClient.getQueryData<{ id: string; name: string }[]>(queryKey); - queryClient.setQueryData<{ id: string; name: string }[]>( + const prev = getQuerySnapshots( + queryClient, queryKey, - (old) => - (old ?? []).map((item) => - item.id === d.id ? { ...item, name: trimmed } : item, - ), + ); + queryClient.setQueriesData({ queryKey }, (old) => + (old ?? []).map((item) => + item.id === d.id ? { ...item, name: trimmed } : item, + ), ); try { await renameDashboard({ id: d.id, name: trimmed }); + queryClient.removeQueries({ queryKey: sqlDashboardPrefetchKey(d.id) }); queryClient.invalidateQueries({ queryKey }); queryClient.invalidateQueries({ queryKey: ["sql-dashboards-palette"], }); } catch (err) { - if (prev) queryClient.setQueryData(queryKey, prev); + restoreQuerySnapshots(queryClient, prev); throw err; } }, @@ -1308,10 +1468,12 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { const handleAnalysisDelete = useCallback( async (a: { id: string; name: string }) => { const queryKey = ["analyses-sidebar"] as const; - const prev = - queryClient.getQueryData<{ id: string; name: string }[]>(queryKey); - queryClient.setQueryData<{ id: string; name: string }[]>( + const prev = getQuerySnapshots<{ id: string; name: string }[]>( + queryClient, queryKey, + ); + queryClient.setQueriesData<{ id: string; name: string }[]>( + { queryKey }, (old) => (old ?? []).filter((item) => item.id !== a.id), ); try { @@ -1323,10 +1485,13 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { if (!res.ok) { throw new Error(`Delete failed: ${res.status}`); } + queryClient.removeQueries({ + queryKey: analysisDetailPrefetchKey(a.id), + }); queryClient.invalidateQueries({ queryKey }); queryClient.invalidateQueries({ queryKey: ["analyses-list"] }); } catch (err) { - if (prev) queryClient.setQueryData(queryKey, prev); + restoreQuerySnapshots(queryClient, prev); throw err; } }, @@ -1341,36 +1506,41 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { const sidebarKey = ["analyses-sidebar"] as const; const listKey = ["analyses-list"] as const; const detailKey = ["analysis-detail", a.id] as const; - const prevSidebar = - queryClient.getQueryData<{ id: string; name: string }[]>(sidebarKey); - const prevList = queryClient.getQueryData(listKey); - const prevDetail = queryClient.getQueryData(detailKey); - - queryClient.setQueryData<{ id: string; name: string }[]>( + const prevSidebar = getQuerySnapshots<{ id: string; name: string }[]>( + queryClient, sidebarKey, + ); + const prevList = getQuerySnapshots(queryClient, listKey); + const prevDetail = getQuerySnapshots(queryClient, detailKey); + + queryClient.setQueriesData<{ id: string; name: string }[]>( + { queryKey: sidebarKey }, (old) => (old ?? []).map((item) => item.id === a.id ? { ...item, name: trimmed } : item, ), ); - queryClient.setQueryData(listKey, (old) => + queryClient.setQueriesData({ queryKey: listKey }, (old) => (old ?? []).map((item) => item.id === a.id ? { ...item, name: trimmed } : item, ), ); - queryClient.setQueryData(detailKey, (old) => + queryClient.setQueriesData({ queryKey: detailKey }, (old) => old ? { ...old, name: trimmed } : old, ); try { await renameAnalysis({ id: a.id, name: trimmed }); + queryClient.removeQueries({ + queryKey: analysisDetailPrefetchKey(a.id), + }); queryClient.invalidateQueries({ queryKey: sidebarKey }); queryClient.invalidateQueries({ queryKey: listKey }); queryClient.invalidateQueries({ queryKey: detailKey }); } catch (err) { - if (prevSidebar) queryClient.setQueryData(sidebarKey, prevSidebar); - if (prevList) queryClient.setQueryData(listKey, prevList); - if (prevDetail) queryClient.setQueryData(detailKey, prevDetail); + restoreQuerySnapshots(queryClient, prevSidebar); + restoreQuerySnapshots(queryClient, prevList); + restoreQuerySnapshots(queryClient, prevDetail); throw err; } }, @@ -1571,13 +1741,14 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { ))} @@ -1724,6 +1895,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { onToggleFavorite={toggleFavorite} onDelete={() => handleAnalysisDelete(a)} onRename={(name) => handleAnalysisRename(a, name)} + onPrefetch={() => prefetchAnalysis(a.id)} /> ))} {sortedAnalyses.length > SIDEBAR_PREVIEW_COUNT && ( diff --git a/templates/analytics/app/lib/prefetch-keys.ts b/templates/analytics/app/lib/prefetch-keys.ts new file mode 100644 index 000000000..1a6d57641 --- /dev/null +++ b/templates/analytics/app/lib/prefetch-keys.ts @@ -0,0 +1,10 @@ +export type PrefetchSnapshot = { + data: T; + syncVersion: number; +}; + +export const sqlDashboardPrefetchKey = (id: string) => + ["data", "sql-dashboard-prefetch", id] as const; + +export const analysisDetailPrefetchKey = (id: string) => + ["analysis-detail-prefetch", id] as const; diff --git a/templates/analytics/app/pages/adhoc/AdhocRouter.tsx b/templates/analytics/app/pages/adhoc/AdhocRouter.tsx index 0e6fbdfec..a692ac1eb 100644 --- a/templates/analytics/app/pages/adhoc/AdhocRouter.tsx +++ b/templates/analytics/app/pages/adhoc/AdhocRouter.tsx @@ -1,12 +1,8 @@ import { Suspense, lazy, useEffect } from "react"; -import { useParams, useSearchParams } from "react-router"; -import { useQuery } from "@tanstack/react-query"; +import { useParams } from "react-router"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; -import { getIdToken } from "@/lib/auth"; import { dashboardComponents } from "./registry"; -import BlankDashboard from "./BlankDashboard"; -import { appApiPath } from "@agent-native/core/client"; import { incrementItemView } from "@/lib/item-popularity"; const SqlDashboardPage = lazy(() => import("./sql-dashboard")); @@ -34,22 +30,7 @@ function DashboardSkeleton() { ); } -function SqlDashboardLoader({ id }: { id: string }) { - const { data: exists, isLoading } = useQuery({ - queryKey: ["sql-dashboard-exists", id], - queryFn: async () => { - const token = await getIdToken(); - const res = await fetch(appApiPath(`/api/sql-dashboards/${id}`), { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); - return res.ok; - }, - staleTime: 30_000, - }); - - if (isLoading) return ; - if (!exists) return ; - +function SqlDashboardLoader() { return ( }> @@ -59,7 +40,6 @@ function SqlDashboardLoader({ id }: { id: string }) { export default function AdhocRouter() { const { id = "default" } = useParams<{ id: string }>(); - const [searchParams] = useSearchParams(); const Component = dashboardComponents[id]; useEffect(() => { @@ -76,8 +56,5 @@ export default function AdhocRouter() { ); } - // Check for SQL dashboard (id passed via URL param, or use the route id) - const sqlId = searchParams.get("id") || id; - - return ; + return ; } diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx b/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx index 252c5796f..5a6ba1004 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx @@ -69,6 +69,7 @@ import { import { interpolate } from "./interpolate"; import { AddPanelPopover, PanelEditorDialog } from "./PanelEditorDialog"; import { ViewsMenu } from "./ViewsMenu"; +import BlankDashboard from "../BlankDashboard"; import { clampDashboardColumns, clampPanelWidth, @@ -79,6 +80,10 @@ import { import { useUserPref } from "@/hooks/use-user-pref"; import { useDashboardViews } from "@/hooks/use-dashboard-views"; import { incrementItemView } from "@/lib/item-popularity"; +import { + sqlDashboardPrefetchKey, + type PrefetchSnapshot, +} from "@/lib/prefetch-keys"; import { DashboardTitleSkeleton, useSetPageTitle, @@ -115,6 +120,7 @@ async function fetchWithAuth(url: string, options?: RequestInit) { } type FetchedDashboard = { + id: string; config: SqlDashboardConfig; archivedAt: string | null; }; @@ -124,6 +130,7 @@ async function fetchDashboard(id: string): Promise { if (!res.ok) return null; const data = await res.json(); return { + id, config: { name: data.name ?? "Untitled Dashboard", description: data.description, @@ -191,8 +198,29 @@ export default function SqlDashboardPage() { if (!dashboardId) return null; return fetchDashboard(dashboardId); }, - staleTime: 2_000, + staleTime: 30_000, placeholderData: (prev) => prev, + initialData: () => { + if (!dashboardId) return undefined; + const snapshot = queryClient.getQueryData< + PrefetchSnapshot + >(sqlDashboardPrefetchKey(dashboardId)); + if (snapshot?.data === null && snapshot.syncVersion !== sync) { + return undefined; + } + return snapshot?.data; + }, + initialDataUpdatedAt: () => { + if (!dashboardId) return undefined; + const queryKey = sqlDashboardPrefetchKey(dashboardId); + const snapshot = + queryClient.getQueryData>( + queryKey, + ); + if (!snapshot) return undefined; + if (snapshot.syncVersion !== sync) return 0; + return queryClient.getQueryState(queryKey)?.dataUpdatedAt; + }, }); // Panel edit dialog state @@ -299,11 +327,8 @@ export default function SqlDashboardPage() { useEffect(() => { if (!dashboardId || !dashboardQuery.isSuccess) return; const fetched = dashboardQuery.data; - const next = fetched?.config ?? { - name: "Untitled Dashboard", - panels: [], - }; - setDashboard(next); + if (fetched && fetched.id !== dashboardId) return; + setDashboard(fetched?.config ?? null); setArchivedAt(fetched?.archivedAt ?? null); setLoaded(true); if (fetched && viewedDashboardIdRef.current !== dashboardId) { @@ -411,6 +436,9 @@ export default function SqlDashboardPage() { pushToCollab(updated); saveDashboard(dashboardId, updated) .then(() => { + queryClient.removeQueries({ + queryKey: sqlDashboardPrefetchKey(dashboardId), + }); queryClient.invalidateQueries({ queryKey: ["sql-dashboards-sidebar"], }); @@ -442,6 +470,9 @@ export default function SqlDashboardPage() { await saveDashboard(dashboardId, updated); setDashboard(updated); pushToCollab(updated); + queryClient.removeQueries({ + queryKey: sqlDashboardPrefetchKey(dashboardId), + }); queryClient.invalidateQueries({ queryKey: ["sql-dashboards-sidebar"] }); queryClient.invalidateQueries({ queryKey: ["sql-dashboards-palette"] }); queryClient.invalidateQueries({ @@ -659,6 +690,9 @@ export default function SqlDashboardPage() { queryKey: ["sql-dashboards-archived-sidebar"], }); queryClient.invalidateQueries({ queryKey: ["sql-dashboards-palette"] }); + queryClient.removeQueries({ + queryKey: sqlDashboardPrefetchKey(dashboardId), + }); queryClient.invalidateQueries({ queryKey: ["data", "sql-dashboard", dashboardId], }); @@ -688,6 +722,9 @@ export default function SqlDashboardPage() { queryClient.invalidateQueries({ queryKey: ["sql-dashboards-archived-sidebar"], }); + queryClient.removeQueries({ + queryKey: sqlDashboardPrefetchKey(dashboardId), + }); queryClient.invalidateQueries({ queryKey: ["data", "sql-dashboard", dashboardId], }); @@ -904,7 +941,7 @@ export default function SqlDashboardPage() { ); } - if (!dashboard) return null; + if (!dashboard) return ; return (
diff --git a/templates/analytics/app/pages/analyses/AnalysisDetail.tsx b/templates/analytics/app/pages/analyses/AnalysisDetail.tsx index 9dce7eeec..4cea6fad1 100644 --- a/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +++ b/templates/analytics/app/pages/analyses/AnalysisDetail.tsx @@ -45,6 +45,10 @@ import { useSetHeaderActions, } from "@/components/layout/HeaderActions"; import { cn } from "@/lib/utils"; +import { + analysisDetailPrefetchKey, + type PrefetchSnapshot, +} from "@/lib/prefetch-keys"; interface Analysis { id: string; @@ -100,6 +104,25 @@ export default function AnalysisDetail() { enabled: !!id, staleTime: 10_000, placeholderData: (prev) => prev, + initialData: () => { + if (!id) return undefined; + const snapshot = queryClient.getQueryData< + PrefetchSnapshot + >(analysisDetailPrefetchKey(id)); + if (snapshot?.data === null && snapshot.syncVersion !== analysesSync) { + return undefined; + } + return snapshot?.data; + }, + initialDataUpdatedAt: () => { + if (!id) return undefined; + const queryKey = analysisDetailPrefetchKey(id); + const snapshot = + queryClient.getQueryData>(queryKey); + if (!snapshot) return undefined; + if (snapshot.syncVersion !== analysesSync) return 0; + return queryClient.getQueryState(queryKey)?.dataUpdatedAt; + }, }); useEffect(() => { @@ -124,6 +147,7 @@ export default function AnalysisDetail() { const handleDelete = async () => { if (!id) return; await deleteAnalysis(id); + queryClient.removeQueries({ queryKey: analysisDetailPrefetchKey(id) }); queryClient.invalidateQueries({ queryKey: ["analyses-list"] }); navigate("/analyses"); };