diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index 5acf9fdf8..7c156b9d6 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -184,7 +184,6 @@ "components/provenance/source-provenance-view.tsx :: SourceProvenanceView": 363, "components/records/allergy-form.tsx :: AllergyForm": 225, "components/records/audit-c-panel.tsx :: AuditCPanel": 285, - "components/records/free-text-take-over.tsx :: FreeTextTakeOver": 201, "components/records/life-circumstances-manager.tsx :: SectionBody": 472, "components/settings/about-section.tsx :: AboutSection": 236, "components/settings/access/grant-invite-card.tsx :: GrantInviteCard": 311, diff --git a/src/components/documents/__tests__/confirm-facts-invalidation.test.ts b/src/components/documents/__tests__/confirm-facts-invalidation.test.ts new file mode 100644 index 000000000..43391c745 --- /dev/null +++ b/src/components/documents/__tests__/confirm-facts-invalidation.test.ts @@ -0,0 +1,34 @@ +/** + * Confirming a document's facts creates medications, conditions and lab + * values. The confirm used to evict only the roots of those lists, so the + * dashboard, the Today digest, adherence, nutrients, the day and the timeline + * kept showing the record without them. + */ +import { QueryClient } from "@tanstack/react-query"; +import { afterEach, describe, expect, it } from "vitest"; + +import { invalidateConfirmedFactReads } from "../use-document-facts"; +import { queryKeys } from "@/lib/query-keys"; + +const client = new QueryClient(); + +afterEach(() => client.clear()); + +describe("invalidateConfirmedFactReads", () => { + it.each([ + ["the dashboard snapshot", queryKeys.dashboardSnapshot()], + ["the Today digest", queryKeys.dailyDigest()], + ["the nutrient reads", queryKeys.nutrientsRoot()], + ["an open day", queryKeys.day("2026-11-10")], + ["the timeline", queryKeys.timelineRoot()], + ["the lab results", queryKeys.labResults()], + ["the condition journal", queryKeys.illness()], + ["the document", queryKeys.inboundDocument("doc-1")], + ])("marks %s stale", async (_label, key) => { + client.setQueryData(key, { seeded: true }); + + await invalidateConfirmedFactReads(client, "doc-1"); + + expect(client.getQueryState(key)?.isInvalidated).toBe(true); + }); +}); diff --git a/src/components/documents/use-document-facts.ts b/src/components/documents/use-document-facts.ts index 2ab18b787..999092376 100644 --- a/src/components/documents/use-document-facts.ts +++ b/src/components/documents/use-document-facts.ts @@ -13,7 +13,11 @@ * approve/reject decisions the person made; a fact without a decision stays * pending on the server. */ -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + useMutation, + useQueryClient, + type QueryClient, +} from "@tanstack/react-query"; import { resolveAiRun, useAiRunPhase } from "@/hooks/use-ai-run"; import { apiPatch, apiPost } from "@/lib/api/api-fetch"; @@ -21,7 +25,15 @@ import type { AiRunAccepted, DocumentExtractRunResult, } from "@/lib/documents/ai-runs/types"; -import { invalidateKeys, queryKeys } from "@/lib/query-keys"; +import { + documentDependentKeys, + illnessDependentKeys, + invalidateKeys, + labDependentKeys, + medicationDependentKeys, + queryKeys, + refetchInactiveDailyReads, +} from "@/lib/query-keys"; import type { ExtractedFactDto, InboundFactEdit, @@ -101,11 +113,32 @@ export function useEditFact() { }); } +/** + * Every read a confirm can touch: the medications, conditions and lab values + * it creates, with what derives from them, and the document itself (status + * and fact states). A new medication or condition also reaches the dashboard + * and the Today digest, which are unmounted while the person reviews. + */ +export function invalidateConfirmedFactReads( + queryClient: QueryClient, + documentId: string, +) { + return Promise.all([ + invalidateKeys(queryClient, [ + ...medicationDependentKeys, + ...illnessDependentKeys, + ...labDependentKeys, + ...documentDependentKeys, + queryKeys.inboundDocument(documentId), + ]), + refetchInactiveDailyReads(queryClient), + ]); +} + /** * Commit the review decisions. Approved facts land in their structured * stores (labs / conditions / medications) through the server's existing - * field-by-field commit; rejected facts are discarded. Invalidates every - * read the commit can touch, plus the document itself (status + fact states). + * field-by-field commit; rejected facts are discarded. */ export function useConfirmFacts() { const queryClient = useQueryClient(); @@ -127,15 +160,7 @@ export function useConfirmFacts() { `/api/documents/inbound/${documentId}/confirm`, { decisions }, ), - onSuccess: (_data, { documentId }) => { - void invalidateKeys(queryClient, [ - queryKeys.inboundDocument(documentId), - queryKeys.documents(), - queryKeys.labResults(), - queryKeys.biomarkers(), - queryKeys.illness(), - queryKeys.medications(), - ]); - }, + onSuccess: (_data, { documentId }) => + void invalidateConfirmedFactReads(queryClient, documentId), }); } diff --git a/src/components/records/free-text-take-over.tsx b/src/components/records/free-text-take-over.tsx index e52f2f608..9574ee6ed 100644 --- a/src/components/records/free-text-take-over.tsx +++ b/src/components/records/free-text-take-over.tsx @@ -31,7 +31,13 @@ import { withBaseToken } from "@/lib/api/optimistic-token"; import { showErrorToast } from "@/lib/errors/error-toast"; import { useTranslations } from "@/lib/i18n/context"; import type { LifeCircumstancesDto } from "@/lib/profile/life-circumstances"; -import { queryKeys } from "@/lib/query-keys"; +import { + allergyDependentKeys, + illnessDependentKeys, + invalidateKeys, + queryKeys, + refetchInactiveDailyReads, +} from "@/lib/query-keys"; import { dateOnlyAtNoonUtc, isCalendarDateKey } from "@/lib/tz/date-only"; interface AboutMeData { @@ -66,13 +72,12 @@ export function FreeTextTakeOver({ const afterCreate = () => { setSaved(true); toastWrittenOutcome("success", t("records.life.takeOver.savedToast")); - void queryClient.invalidateQueries({ - queryKey: queryKeys.lifeCircumstances(), - }); - void queryClient.invalidateQueries({ - queryKey: - field === "conditions" ? queryKeys.illness() : queryKeys.allergies(), - }); + void invalidateKeys(queryClient, [ + queryKeys.lifeCircumstances(), + ...(field === "conditions" ? illnessDependentKeys : allergyDependentKeys), + ]); + // A chronic condition frames the dashboard and the Today digest. + if (field === "conditions") void refetchInactiveDailyReads(queryClient); }; const createCondition = useMutation({ @@ -103,12 +108,10 @@ export function FreeTextTakeOver({ onSuccess: () => { toastWrittenOutcome("success", t("records.life.takeOver.clearedToast")); close(); - void queryClient.invalidateQueries({ - queryKey: queryKeys.coachAboutMe(), - }); - void queryClient.invalidateQueries({ - queryKey: queryKeys.lifeCircumstances(), - }); + void invalidateKeys(queryClient, [ + queryKeys.coachAboutMe(), + queryKeys.lifeCircumstances(), + ]); }, onError: (error) => showErrorToast(t("records.life.takeOver.clearError"), { cause: error }),