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.
This commit is contained in:
Marc-André Bombeck committed 2026-11-12 12:51:32 +01:00
1 parent 92e1e24599
commit 671245c782
15 files changed
+83 -37

No files matched your search

+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -12490,7 +12490,8 @@
"purgeConfirmBody": "대기 기간이 끝나기 전에 지금 삭제되며 복구할 수 없습니다."
},
"picker": {
"capped": "최신 {count}개를 보여 줘요. 이전 문서는 검색으로 찾아 주세요."
"capped": "최신 {count}개를 보여 줘요. 이전 문서는 검색으로 찾아 주세요.",
"cappedSearching": "최근 검색 결과 {count}개를 보여 줘요. 이전 문서는 검색어를 좁혀서 찾아 주세요."
}
},
"selfDescription": {
+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -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": {
+2 -1
View File
@@ -12490,7 +12490,8 @@
"purgeConfirmBody": "Он будет удалён сейчас, а не в конце периода ожидания, и его нельзя будет восстановить."
},
"picker": {
"capped": "Показаны {count} самых новых. Более старый документ найдите через поиск."
"capped": "Показаны {count} самых новых. Более старый документ найдите через поиск.",
"cappedSearching": "Показаны {count} самых новых совпадений. Уточните поиск, чтобы найти более старый документ."
}
},
"selfDescription": {
@@ -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(
<I18nProvider initialLocale="en">
<DocumentPickerCapNotice cut count={50} searching />
</I18nProvider>,
);
expect(html).toContain("Narrow the search");
expect(html).not.toContain("Search to find");
});
});
@@ -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({
})}
</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}
<DocumentPickerCapNotice
cut={cut && !list.isError}
count={documents.length}
searching={query !== ""}
/>
</div>
</ResponsiveSheet>
);
}
/**
* 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 (
<ListCapNotice
cap={{ truncated: cut, limit: count }}
message={t(
searching
? "documents.picker.cappedSearching"
: "documents.picker.capped",
{ count: String(count) },
)}
/>
);
}
@@ -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);
@@ -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({
})}
</ul>
)}
{cut && !list.isError ? (
<p
data-slot="coach-attachment-picker-capped"
className="text-muted-foreground text-xs"
>
{t("documents.picker.capped", {
count: String(list.data?.documents.length ?? 0),
})}
</p>
) : null}
<DocumentPickerCapNotice
cut={cut && !list.isError}
count={list.data?.documents.length ?? 0}
searching={query !== ""}
/>
</div>
</ResponsiveSheet>
);
+5 -2
View File
@@ -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) })}
</p>
);
}