diff --git a/messages/de.json b/messages/de.json index 4e0812d27..31dee6efe 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Das Dokument konnte nicht gelöscht werden.", "purgeConfirmTitle": "Dieses Dokument endgültig löschen?", "purgeConfirmBody": "Es wird jetzt entfernt statt am Ende der Wartezeit und lässt sich nicht wiederherstellen." + }, + "picker": { + "capped": "Hier stehen die neuesten {count}. Ältere Dokumente findest du über die Suche." } }, "selfDescription": { diff --git a/messages/en.json b/messages/en.json index aa3c7dcd8..bda36e4f1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Couldn't delete the document.", "purgeConfirmTitle": "Delete this document for good?", "purgeConfirmBody": "It is removed now instead of at the end of the waiting period, and cannot be restored." + }, + "picker": { + "capped": "Showing the newest {count}. Search to find an older document." } }, "selfDescription": { diff --git a/messages/es.json b/messages/es.json index edf57f570..66b49ced7 100644 --- a/messages/es.json +++ b/messages/es.json @@ -12491,6 +12491,9 @@ "purgeFailed": "No se pudo eliminar el documento.", "purgeConfirmTitle": "¿Eliminar este documento definitivamente?", "purgeConfirmBody": "Se elimina ahora en lugar de al final del periodo de espera y no se puede restaurar." + }, + "picker": { + "capped": "Se muestran los {count} más recientes. Usa la búsqueda para encontrar un documento más antiguo." } }, "selfDescription": { diff --git a/messages/fr.json b/messages/fr.json index f44944f05..496a689ea 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Impossible de supprimer le document.", "purgeConfirmTitle": "Supprimer ce document définitivement ?", "purgeConfirmBody": "Il est supprimé maintenant plutôt qu'à la fin du délai d'attente et ne peut pas être restauré." + }, + "picker": { + "capped": "Les {count} plus récents sont affichés. Utilisez la recherche pour trouver un document plus ancien." } }, "selfDescription": { diff --git a/messages/it.json b/messages/it.json index ca17d5d5d..f0291f68c 100644 --- a/messages/it.json +++ b/messages/it.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Impossibile eliminare il documento.", "purgeConfirmTitle": "Eliminare definitivamente questo documento?", "purgeConfirmBody": "Viene rimosso ora anziché alla fine del periodo di attesa e non può essere ripristinato." + }, + "picker": { + "capped": "Sono mostrati i {count} più recenti. Usa la ricerca per trovare un documento più vecchio." } }, "selfDescription": { diff --git a/messages/ko.json b/messages/ko.json index 3dd8f5bb9..d725f3894 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12491,6 +12491,9 @@ "purgeFailed": "문서를 삭제하지 못했어요.", "purgeConfirmTitle": "이 문서를 완전히 삭제할까요?", "purgeConfirmBody": "대기 기간이 끝나기 전에 지금 삭제되며 복구할 수 없습니다." + }, + "picker": { + "capped": "최신 {count}개를 보여 줘요. 이전 문서는 검색으로 찾아 주세요." } }, "selfDescription": { diff --git a/messages/nl.json b/messages/nl.json index cfe154d6e..eb10a6fe9 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Het document kon niet worden verwijderd.", "purgeConfirmTitle": "Dit document definitief verwijderen?", "purgeConfirmBody": "Het wordt nu verwijderd in plaats van aan het einde van de wachttijd en kan niet worden hersteld." + }, + "picker": { + "capped": "De nieuwste {count} staan hier. Zoek om een ouder document te vinden." } }, "selfDescription": { diff --git a/messages/pl.json b/messages/pl.json index b426ef853..a68ec2c75 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Nie udało się usunąć dokumentu.", "purgeConfirmTitle": "Usunąć ten dokument na stałe?", "purgeConfirmBody": "Zostanie usunięty teraz zamiast po okresie oczekiwania i nie będzie można go przywrócić." + }, + "picker": { + "capped": "Widać {count} najnowszych. Starszy dokument znajdziesz przez wyszukiwanie." } }, "selfDescription": { diff --git a/messages/pt-BR.json b/messages/pt-BR.json index ce959a62c..46ffffbe4 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Não foi possível excluir o documento.", "purgeConfirmTitle": "Excluir este documento para sempre?", "purgeConfirmBody": "Ele é removido agora, e não no fim do período de espera, e não pode ser restaurado." + }, + "picker": { + "capped": "Mostrando os {count} mais recentes. Use a busca para encontrar um documento mais antigo." } }, "selfDescription": { diff --git a/messages/ru.json b/messages/ru.json index d98b8a0b2..786103b21 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -12491,6 +12491,9 @@ "purgeFailed": "Не удалось удалить документ.", "purgeConfirmTitle": "Удалить этот документ навсегда?", "purgeConfirmBody": "Он будет удалён сейчас, а не в конце периода ожидания, и его нельзя будет восстановить." + }, + "picker": { + "capped": "Показаны {count} самых новых. Более старый документ найдите через поиск." } }, "selfDescription": { diff --git a/src/__tests__/client-fetch-limit-guard.test.ts b/src/__tests__/client-fetch-limit-guard.test.ts index 95e2363c7..514275f8b 100644 --- a/src/__tests__/client-fetch-limit-guard.test.ts +++ b/src/__tests__/client-fetch-limit-guard.test.ts @@ -83,7 +83,7 @@ const ALLOWLIST: readonly Allowed[] = [ file: "components/documents/document-picker-sheet.tsx", limit: 50, reason: - "Search-backed picker (episode links and share links): the query narrows the whole document vault server-side.", + "Search-backed picker (episode links and share links): the query narrows the whole document vault server-side, and a cut list says so and points to the search.", }, { file: "hooks/use-mood-day.ts", diff --git a/src/components/documents/__tests__/document-picker-sheet-cap.test.tsx b/src/components/documents/__tests__/document-picker-sheet-cap.test.tsx new file mode 100644 index 000000000..a698cb5be --- /dev/null +++ b/src/components/documents/__tests__/document-picker-sheet-cap.test.tsx @@ -0,0 +1,71 @@ +/** + * The document picker reads one page of the vault, the newest fifty. With a + * longer vault it listed those and said nothing, so an older report seemed + * not to exist unless one already knew to search for it. + * + * ResponsiveSheet portals its body, which static markup does not render, so it + * is mocked down to a wrapper that always shows its children. + */ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +const page = vi.hoisted(() => ({ nextCursor: null as string | null })); + +vi.mock("@/components/ui/responsive-sheet", () => ({ + ResponsiveSheet: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ + data: { + // A cut page is a full one. + documents: Array.from({ length: page.nextCursor ? 50 : 3 }, (_, i) => ({ + id: `d${i}`, + kind: "LAB_RESULT", + title: `Blood panel ${i}`, + filename: null, + documentDate: "2026-10-01", + createdAt: "2026-10-01T08:00:00.000Z", + })), + nextCursor: page.nextCursor, + }, + isPending: false, + isError: false, + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { DocumentPickerSheet } from "../document-picker-sheet"; + +function render() { + return renderToStaticMarkup( + + undefined} + title="Link documents" + description="Pick documents" + searchPlaceholder="Search" + emptyLabel="No documents" + isPicked={() => false} + onToggle={() => undefined} + /> + , + ); +} + +describe("DocumentPickerSheet", () => { + it("says the list is cut, and how to reach the rest", () => { + page.nextCursor = "d49"; + const html = render(); + expect(html).toContain('data-slot="document-picker-capped"'); + expect(html).toContain("Showing the newest 50"); + }); + + it("says nothing when every document is listed", () => { + page.nextCursor = null; + expect(render()).not.toContain('data-slot="document-picker-capped"'); + }); +}); diff --git a/src/components/documents/document-picker-sheet.tsx b/src/components/documents/document-picker-sheet.tsx index 6794e3650..a14908f5a 100644 --- a/src/components/documents/document-picker-sheet.tsx +++ b/src/components/documents/document-picker-sheet.tsx @@ -3,8 +3,9 @@ /** * The vault document picker sheet: a `ResponsiveSheet` (bottom sheet on * phones) with a debounced search over the person's own vault — the same - * recall path as the vault page, one page (50) of matches, the search input - * as the way to an older document — and one toggle row per document. + * recall path as the vault page, one page (50) of matches with a line that + * says when more exist, the search input as the way to an older document — + * and one toggle row per document. * * Two pickers used to carry this whole body as copies: linking documents to * an illness episode (each toggle writes at once) and choosing documents for @@ -97,6 +98,8 @@ export function DocumentPickerSheet({ }); const documents = list.data?.documents ?? []; + // More match than one page: say so, and that the search reaches the rest. + const cut = Boolean(list.data?.nextCursor); return ( )} + {cut && !list.isError ? ( +

+ {t("documents.picker.capped", { + count: String(documents.length), + })} +

+ ) : null}
);