fix(query-keys): refresh the day view and timeline after every write

Symptoms, workouts, lab results, allergies, documents and questionnaires
evicted only their own lists. An open day, its calendar dots and the
timeline kept the pre-write answer for up to five minutes, so "Capture
for this day" saved a symptom or a workout and the day showed the old
list, and a deleted row stayed on it.

Workouts, labs, allergies and documents get a bundle beside the seven
that already carry the day and timeline roots; the six lab lists become
one (the OCR dialog's own copy of the commit hook's list goes with it).
Symptoms and questionnaires add the two roots to their list. Symptoms
stay off the illness bundle on purpose: nothing on the dashboard or the
Today digest reads them. Custom metrics need nothing: neither the day
nor the timeline reads them. invalidateKeys now drops a key listed twice,
since spread bundles share the two roots.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 19:41:14 +01:00
1 parent aae029d94e
commit 2e73b0ffec
24 files changed
+257 -118

No files matched your search

@@ -91,7 +91,6 @@ function render(
mode={options.mode ?? "vision"}
pdfSupported={options.pdfSupported ?? true}
consentRequired={options.consentRequired}
onCommitted={() => {}}
/>
</I18nProvider>
</QueryClientProvider>,
+3 -5
View File
@@ -16,7 +16,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch";
import { formatReferenceRange } from "@/lib/labs/reference-range";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { invalidateKeys, labDependentKeys, queryKeys } from "@/lib/query-keys";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
@@ -68,8 +68,7 @@ export function BiomarkerManager() {
mutationFn: (id: string) => apiDelete(`/api/biomarkers/${id}`),
onSuccess: () => {
toast.success(t("labs.biomarker.deletedToast"));
queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() });
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
void invalidateKeys(queryClient, labDependentKeys);
},
onError: (failure) =>
showErrorToast(t("labs.biomarker.deleteError"), { cause: failure }),
@@ -103,8 +102,7 @@ export function BiomarkerManager() {
function afterSave() {
setFormOpen(false);
setEditing(null);
queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() });
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
void invalidateKeys(queryClient, labDependentKeys);
}
const markers = data?.biomarkers ?? [];
@@ -25,7 +25,7 @@ import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { TagChip } from "@/components/ui/tag-chip";
import { apiPost } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { invalidateKeys, labDependentKeys } from "@/lib/query-keys";
import type { BiomarkerDto, BiomarkerMergeResultDto } from "./types";
import { labUnitLabel } from "@/lib/labs/dimensionless";
@@ -79,8 +79,7 @@ export function BiomarkerMergeSheet({
name: result.target.name,
}),
);
queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() });
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
void invalidateKeys(queryClient, labDependentKeys);
reset();
onOpenChange(false);
},
+10 -15
View File
@@ -44,7 +44,7 @@ import {
type MessageBundle,
} from "@/lib/i18n/load-locale";
import { resolveKey } from "@/lib/i18n/resolve-key";
import { queryKeys } from "@/lib/query-keys";
import { invalidateKeys, labDependentKeys, queryKeys } from "@/lib/query-keys";
import dynamic from "next/dynamic";
import type { ComponentProps } from "react";
@@ -259,13 +259,10 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
// lapsed; bust them alongside the result list so the assessment repaints as
// soon as the reading lands.
function invalidateAfterReadingWrite() {
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
queryClient.invalidateQueries({
queryKey: queryKeys.insightsBiomarkerAssessment(biomarkerId, locale),
});
queryClient.invalidateQueries({
queryKey: queryKeys.insightsLabsChanges(),
});
void invalidateKeys(queryClient, [
...labDependentKeys,
queryKeys.insightsBiomarkerAssessment(biomarkerId, locale),
]);
}
function afterAdd() {
@@ -282,12 +279,11 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
function afterEditMarker() {
setEditOpen(false);
queryClient.invalidateQueries({
queryKey: queryKeys.biomarkerDetail(biomarkerId),
});
queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() });
// Resolved name / unit / range on every reading derives from the marker.
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
void invalidateKeys(queryClient, [
...labDependentKeys,
queryKeys.biomarkerDetail(biomarkerId),
]);
}
// v1.18.9 (#41/#3) — delete the biomarker directly from its detail page so
@@ -301,8 +297,7 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
mutationFn: () => apiDelete(`/api/biomarkers/${biomarkerId}`),
onSuccess: () => {
toast.success(t("labs.biomarker.deletedToast"));
queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() });
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
void invalidateKeys(queryClient, labDependentKeys);
router.push("/labs");
},
onError: (failure) =>
+2 -9
View File
@@ -35,7 +35,7 @@ import { LabConvertedNote } from "./lab-converted-note";
import { resolveNoteForUpdate } from "@/lib/labs/note-update";
import { resolveValueForUpdate, valueEditSeed } from "@/lib/labs/value-update";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { invalidateKeys, labDependentKeys, queryKeys } from "@/lib/query-keys";
import { ReferenceRangeBadge } from "./reference-range-badge";
import { LabReferenceRangeBar } from "./lab-reference-range-bar";
@@ -147,14 +147,7 @@ export function LabHistoryList({ readings }: { readings: LabResultDto[] }) {
const [hasNote, setHasNote] = useState(false);
function invalidate() {
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
// v1.32.19 — a lab reading edit / delete / restore shifts the two most
// recent panels the Insights "what changed since your last panel" card
// reads, so bust it too instead of leaving it on its pre-write deltas
// until the 60 s stale window lapses.
queryClient.invalidateQueries({
queryKey: queryKeys.insightsLabsChanges(),
});
void invalidateKeys(queryClient, labDependentKeys);
}
async function restore(id: string) {
@@ -142,7 +142,6 @@ export function OcrReviewDialog({
mode,
pdfSupported,
consentRequired = false,
onCommitted,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -155,7 +154,6 @@ export function OcrReviewDialog({
* granted `/api/auth/me` refreshes and the picker appears.
*/
consentRequired?: boolean;
onCommitted: () => void;
}) {
const { t } = useTranslations();
const pickerId = useId();
@@ -335,7 +333,6 @@ export function OcrReviewDialog({
})
: t("labs.ocr.savedEmptyToast"),
);
onCommitted();
handleClose(false);
},
onError: (failure) =>
+7 -6
View File
@@ -29,7 +29,9 @@ import type { AiRunAccepted } from "@/lib/documents/ai-runs/types";
import { ocrImageToText } from "@/lib/labs/local-ocr";
import {
aiInputDependentKeys,
documentDependentKeys,
invalidateKeys,
labDependentKeys,
queryKeys,
} from "@/lib/query-keys";
import type {
@@ -259,12 +261,11 @@ export function useOcrCommit() {
});
},
onSuccess: (_result, { file }) => {
queryClient.invalidateQueries({ queryKey: queryKeys.labResults() });
queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() });
// A scan filed into the vault adds a document — refresh its lists.
if (file) {
queryClient.invalidateQueries({ queryKey: queryKeys.documents() });
}
// A scan filed into the vault adds a document as well.
void invalidateKeys(queryClient, [
...labDependentKeys,
...(file ? documentDependentKeys : []),
]);
},
});
}