fix(documents): say when the document picker list is cut

The picker for linking documents to an illness and for choosing
documents for a share link reads the newest fifty and dropped the
route's nextCursor, so with a longer vault an older report seemed not
to exist unless one already knew to search for it. A cut list now ends
with a line that says how many are shown and that the search reaches
the rest. Paging was left out: the coach attachment picker shares the
picker's cache key with a single-page read.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 20:08:24 +01:00
1 parent d204b4344b
commit 53f4ecc2c5
13 files changed
+117 -3

No files matched your search

@@ -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 }) => (
<div data-slot="mock-sheet">{children}</div>
),
}));
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(
<I18nProvider initialLocale="en">
<DocumentPickerSheet
open
onOpenChange={() => undefined}
title="Link documents"
description="Pick documents"
searchPlaceholder="Search"
emptyLabel="No documents"
isPicked={() => false}
onToggle={() => undefined}
/>
</I18nProvider>,
);
}
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"');
});
});
@@ -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 (
<ResponsiveSheet
@@ -188,6 +191,16 @@ export function DocumentPickerSheet({
})}
</ul>
)}
{cut && !list.isError ? (
<p
data-slot="document-picker-capped"
className="text-muted-foreground text-xs"
>
{t("documents.picker.capped", {
count: String(documents.length),
})}
</p>
) : null}
</div>
</ResponsiveSheet>
);