diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index ec9a32f35..7b03fc37e 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", - "count": 338, - "total": 109885, + "count": 337, + "total": 109677, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275, @@ -99,7 +99,6 @@ "components/documents/sources/document-source-picker.tsx :: DocumentSourcePicker": 563, "components/illness/log-day-sheet.tsx :: LogDaySheet": 265, "components/illness/new-episode-sheet.tsx :: NewEpisodeSheet": 264, - "components/insights/coach-panel/attachment-picker.tsx :: AttachmentPicker": 208, "components/insights/coach-panel/chat-bubble.tsx :: ChatBubbleImpl": 419, "components/insights/coach-panel/coach-conversation.tsx :: CoachConversation": 1002, "components/insights/coach-panel/coach-input.tsx :: CoachInput": 259, diff --git a/src/__tests__/client-fetch-limit-guard.test.ts b/src/__tests__/client-fetch-limit-guard.test.ts index 514275f8b..13eae31a4 100644 --- a/src/__tests__/client-fetch-limit-guard.test.ts +++ b/src/__tests__/client-fetch-limit-guard.test.ts @@ -77,7 +77,7 @@ const ALLOWLIST: readonly Allowed[] = [ file: "components/insights/coach-panel/attachment-picker.tsx", limit: 50, reason: - "Search-backed picker: the query narrows the whole document vault server-side.", + "Search-backed picker: the query narrows the whole document vault server-side, and a cut list says so and points to the search.", }, { file: "components/documents/document-picker-sheet.tsx", diff --git a/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx b/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx new file mode 100644 index 000000000..a4d117b05 --- /dev/null +++ b/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx @@ -0,0 +1,71 @@ +/** + * The Coach's "Choose from documents" 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, + hasContentIndex: true, + })), + nextCursor: page.nextCursor, + }, + isPending: false, + isError: false, + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { AttachmentPicker } from "../attachment-picker"; + +function render() { + return renderToStaticMarkup( + + undefined} + remainingSlots={3} + excludeIds={[]} + onConfirm={() => undefined} + /> + , + ); +} + +describe("AttachmentPicker", () => { + it("says the list is cut, and how to reach the rest", () => { + page.nextCursor = "d49"; + const html = render(); + expect(html).toContain('data-slot="coach-attachment-picker-capped"'); + expect(html).toContain("Showing the newest 50"); + }); + + it("says nothing when every document is listed", () => { + page.nextCursor = null; + const html = render(); + expect(html).not.toContain('data-slot="coach-attachment-picker-capped"'); + expect( + html.match(/data-slot="coach-attachment-picker-item"/g), + ).toHaveLength(3); + }); +}); diff --git a/src/components/insights/coach-panel/attachment-picker.tsx b/src/components/insights/coach-panel/attachment-picker.tsx index e733d825f..b86fd9fa1 100644 --- a/src/components/insights/coach-panel/attachment-picker.tsx +++ b/src/components/insights/coach-panel/attachment-picker.tsx @@ -45,6 +45,58 @@ export interface AttachmentPickerProps { onConfirm: (ids: string[]) => void; } +/** One document in the picker: a toggle, disabled until it is indexed. */ +function PickerRow({ + doc, + isSelected, + disabled, + onToggle, +}: { + doc: InboundDocumentDto; + isSelected: boolean; + disabled: boolean; + onToggle: () => void; +}) { + const { t } = useTranslations(); + const title = doc.title ?? doc.filename ?? t("documents.card.untitled"); + const Icon = DOCUMENT_KIND_ICONS[doc.kind]; + return ( +
  • + +
  • + ); +} + export function AttachmentPicker({ open, onOpenChange, @@ -107,6 +159,8 @@ export function AttachmentPicker({ ); const capReached = selected.length >= remainingSlots; + // More match than one page: say so, and that the search reaches the rest. + const cut = Boolean(list.data?.nextCursor); function toggle(id: string) { setSelected((prev) => { @@ -200,55 +254,28 @@ export function AttachmentPicker({ )} + {cut && !list.isError ? ( +

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

    + ) : null} );