From 671245c7828b40f2696fa2c3636d2e013ef9b3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 12 Nov 2026 12:51:32 +0100 Subject: [PATCH] fix(documents): one cap line for both pickers, with a search-aware hint The vault picker and the Coach's attachment picker each wrote their own "showing the newest 50" line, which the UI standards leave to `ListCapNotice`, and with a search running past fifty matches the line still said "Search to find an older document". Both pickers now share `DocumentPickerCapNotice` on `ListCapNotice`, which takes the sentence to show, and a running search asks for a narrower one. --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/ko.json | 3 +- messages/nl.json | 3 +- messages/pl.json | 3 +- messages/pt-BR.json | 3 +- messages/ru.json | 3 +- .../document-picker-sheet-cap.test.tsx | 19 ++++++-- .../documents/document-picker-sheet.tsx | 44 ++++++++++++++----- .../__tests__/attachment-picker-cap.test.tsx | 4 +- .../coach-panel/attachment-picker.tsx | 16 +++---- src/components/ui/list-cap-notice.tsx | 7 ++- 15 files changed, 83 insertions(+), 37 deletions(-) diff --git a/messages/de.json b/messages/de.json index fde3de6a4..1e9dc7a3b 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12490,7 +12490,8 @@ "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." + "capped": "Hier stehen die neuesten {count}. Ältere Dokumente findest du über die Suche.", + "cappedSearching": "Hier stehen die neuesten {count} Treffer. Ältere findest du mit einer genaueren Suche." } }, "selfDescription": { diff --git a/messages/en.json b/messages/en.json index 1413b1660..1a5813a7f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12490,7 +12490,8 @@ "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." + "capped": "Showing the newest {count}. Search to find an older document.", + "cappedSearching": "Showing the newest {count} matches. Narrow the search to find an older one." } }, "selfDescription": { diff --git a/messages/es.json b/messages/es.json index b45971f95..24e1a2b7f 100644 --- a/messages/es.json +++ b/messages/es.json @@ -12490,7 +12490,8 @@ "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." + "capped": "Se muestran los {count} más recientes. Usa la búsqueda para encontrar un documento más antiguo.", + "cappedSearching": "Se muestran las {count} coincidencias más recientes. Afina la búsqueda para encontrar una más antigua." } }, "selfDescription": { diff --git a/messages/fr.json b/messages/fr.json index 855209176..81372b683 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12490,7 +12490,8 @@ "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." + "capped": "Les {count} plus récents sont affichés. Utilisez la recherche pour trouver un document plus ancien.", + "cappedSearching": "Les {count} résultats les plus récents sont affichés. Affinez la recherche pour en trouver un plus ancien." } }, "selfDescription": { diff --git a/messages/it.json b/messages/it.json index 5239964c4..393f83a57 100644 --- a/messages/it.json +++ b/messages/it.json @@ -12490,7 +12490,8 @@ "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." + "capped": "Sono mostrati i {count} più recenti. Usa la ricerca per trovare un documento più vecchio.", + "cappedSearching": "Sono mostrati i {count} risultati più recenti. Restringi la ricerca per trovarne uno più vecchio." } }, "selfDescription": { diff --git a/messages/ko.json b/messages/ko.json index dcf508950..c02d82e0a 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12490,7 +12490,8 @@ "purgeConfirmBody": "대기 기간이 끝나기 전에 지금 삭제되며 복구할 수 없습니다." }, "picker": { - "capped": "최신 {count}개를 보여 줘요. 이전 문서는 검색으로 찾아 주세요." + "capped": "최신 {count}개를 보여 줘요. 이전 문서는 검색으로 찾아 주세요.", + "cappedSearching": "최근 검색 결과 {count}개를 보여 줘요. 이전 문서는 검색어를 좁혀서 찾아 주세요." } }, "selfDescription": { diff --git a/messages/nl.json b/messages/nl.json index 6f55cbc18..4e9615f22 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -12490,7 +12490,8 @@ "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." + "capped": "De nieuwste {count} staan hier. Zoek om een ouder document te vinden.", + "cappedSearching": "De nieuwste {count} resultaten staan hier. Verfijn de zoekopdracht om een ouder document te vinden." } }, "selfDescription": { diff --git a/messages/pl.json b/messages/pl.json index 6c50a74cb..0a2b41502 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -12490,7 +12490,8 @@ "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." + "capped": "Widać {count} najnowszych. Starszy dokument znajdziesz przez wyszukiwanie.", + "cappedSearching": "Widać {count} najnowszych wyników. Zawęź wyszukiwanie, aby znaleźć starszy." } }, "selfDescription": { diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 723292f5f..41ad972f5 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -12490,7 +12490,8 @@ "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." + "capped": "Mostrando os {count} mais recentes. Use a busca para encontrar um documento mais antigo.", + "cappedSearching": "Mostrando os {count} resultados mais recentes. Refine a busca para encontrar um mais antigo." } }, "selfDescription": { diff --git a/messages/ru.json b/messages/ru.json index 0d5dc0168..5d8632c7c 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -12490,7 +12490,8 @@ "purgeConfirmBody": "Он будет удалён сейчас, а не в конце периода ожидания, и его нельзя будет восстановить." }, "picker": { - "capped": "Показаны {count} самых новых. Более старый документ найдите через поиск." + "capped": "Показаны {count} самых новых. Более старый документ найдите через поиск.", + "cappedSearching": "Показаны {count} самых новых совпадений. Уточните поиск, чтобы найти более старый документ." } }, "selfDescription": { diff --git a/src/components/documents/__tests__/document-picker-sheet-cap.test.tsx b/src/components/documents/__tests__/document-picker-sheet-cap.test.tsx index a698cb5be..db8b0d9c4 100644 --- a/src/components/documents/__tests__/document-picker-sheet-cap.test.tsx +++ b/src/components/documents/__tests__/document-picker-sheet-cap.test.tsx @@ -37,7 +37,10 @@ vi.mock("@tanstack/react-query", () => ({ })); import { I18nProvider } from "@/lib/i18n/context"; -import { DocumentPickerSheet } from "../document-picker-sheet"; +import { + DocumentPickerCapNotice, + DocumentPickerSheet, +} from "../document-picker-sheet"; function render() { return renderToStaticMarkup( @@ -60,12 +63,22 @@ 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('data-slot="list-cap-notice"'); 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"'); + expect(render()).not.toContain('data-slot="list-cap-notice"'); + }); + + it("asks for a narrower search, not a search, while one runs", () => { + const html = renderToStaticMarkup( + + + , + ); + expect(html).toContain("Narrow the search"); + expect(html).not.toContain("Search to find"); }); }); diff --git a/src/components/documents/document-picker-sheet.tsx b/src/components/documents/document-picker-sheet.tsx index a14908f5a..41bf5f182 100644 --- a/src/components/documents/document-picker-sheet.tsx +++ b/src/components/documents/document-picker-sheet.tsx @@ -18,6 +18,7 @@ import { Check, Search } from "lucide-react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { useDocumentDate } from "@/components/documents/use-document-date"; +import { ListCapNotice } from "@/components/ui/list-cap-notice"; import { DOCUMENT_KIND_ICONS } from "@/components/documents/document-kind-meta"; import { EmptyState } from "@/components/ui/empty-state"; import { Input } from "@/components/ui/input"; @@ -191,17 +192,40 @@ export function DocumentPickerSheet({ })} )} - {cut && !list.isError ? ( -

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

- ) : null} + ); } + +/** + * The line under a picker list cut at one page: the search reaches the rest, + * and while one runs, a narrower one does. Shared by the vault picker and + * the Coach's attachment picker. + */ +export function DocumentPickerCapNotice({ + cut, + count, + searching, +}: { + cut: boolean; + count: number; + searching: boolean; +}) { + const { t } = useTranslations(); + return ( + + ); +} 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 index a4d117b05..202477891 100644 --- a/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx +++ b/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx @@ -56,14 +56,14 @@ 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('data-slot="list-cap-notice"'); 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).not.toContain('data-slot="list-cap-notice"'); 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 b86fd9fa1..0fefc9684 100644 --- a/src/components/insights/coach-panel/attachment-picker.tsx +++ b/src/components/insights/coach-panel/attachment-picker.tsx @@ -15,6 +15,7 @@ import { cn } from "@/lib/utils"; import type { InboundDocumentDto } from "@/lib/validations/inbound-documents"; import { DOCUMENT_KIND_ICONS } from "@/components/documents/document-kind-meta"; +import { DocumentPickerCapNotice } from "@/components/documents/document-picker-sheet"; import { QueryErrorRow } from "@/components/ui/query-error-row"; import { EmptyState } from "@/components/ui/empty-state"; @@ -266,16 +267,11 @@ export function AttachmentPicker({ })} )} - {cut && !list.isError ? ( -

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

- ) : null} + ); diff --git a/src/components/ui/list-cap-notice.tsx b/src/components/ui/list-cap-notice.tsx index c1fe6160b..1664fc6de 100644 --- a/src/components/ui/list-cap-notice.tsx +++ b/src/components/ui/list-cap-notice.tsx @@ -8,14 +8,17 @@ import { cn } from "@/lib/utils"; * The line under a list the server cut at its ceiling. Renders nothing for a * complete list, so a surface passes the cap it read (`apiGetList`) and * never decides on its own whether to show it. Meta tier: one muted sentence - * at the `text-xs` floor. + * at the `text-xs` floor. `message` replaces the generic sentence where the + * surface can say how to reach the rest (a search). */ export function ListCapNotice({ cap, className, + message, }: { cap: ListCap | null | undefined; className?: string; + message?: string; }) { const { t } = useTranslations(); if (!cap?.truncated) return null; @@ -24,7 +27,7 @@ export function ListCapNotice({ data-slot="list-cap-notice" className={cn("text-muted-foreground text-xs", className)} > - {t("common.listCapped", { count: String(cap.limit) })} + {message ?? t("common.listCapped", { count: String(cap.limit) })}

); }