diff --git a/admin/src/definitions/DefinitionsTab.css b/admin/src/definitions/DefinitionsTab.css index 39a275a..121b384 100644 --- a/admin/src/definitions/DefinitionsTab.css +++ b/admin/src/definitions/DefinitionsTab.css @@ -642,4 +642,6 @@ .defs-show-more { align-self: flex-start; + display: flex; + gap: 8px; } diff --git a/admin/src/definitions/DefinitionsTab.test.tsx b/admin/src/definitions/DefinitionsTab.test.tsx index 5cc6f30..aede057 100644 --- a/admin/src/definitions/DefinitionsTab.test.tsx +++ b/admin/src/definitions/DefinitionsTab.test.tsx @@ -1609,7 +1609,7 @@ describe("DefinitionsTab Font Awesome search", () => { expect(within(s).queryByRole("radiogroup", { name: "Font Awesome search results" })).toBeNull(); }); - it("a query swaps the quick picks for bounded, labelled results with Show more", async () => { + it("a query swaps the quick picks for one bounded, labelled page of results", async () => { await renderReady(); fireEvent.click(screen.getByRole("button", { name: "New" })); const s = sheet(); @@ -1619,10 +1619,17 @@ describe("DefinitionsTab Font Awesome search", () => { expect(within(s).queryByRole("radiogroup", { name: "Font Awesome" })).toBeNull(); expect(resultKeys(s)).toHaveLength(40); expect(within(results(s)).getAllByRole("radio")[0].textContent).toBe(searchFaIcons("a")[0].label); - expect(within(s).getByText(`Showing 40 of ${total}`)).toBeTruthy(); + expect(within(s).getByText(`Showing 1–40 of ${total}`)).toBeTruthy(); + expect(within(s).queryByRole("button", { name: "Previous" })).toBeNull(); + // "Show more" replaces the page rather than appending to it. + const firstPage = resultKeys(s); fireEvent.click(within(s).getByRole("button", { name: "Show more" })); - expect(resultKeys(s)).toHaveLength(80); + expect(resultKeys(s)).toEqual(searchFaIcons("a").slice(40, 80).map((icon) => `fa:${icon.name}`)); + expect(resultKeys(s).some((key) => firstPage.includes(key))).toBe(false); + expect(within(s).getByText(`Showing 41–80 of ${total}`)).toBeTruthy(); + fireEvent.click(within(s).getByRole("button", { name: "Previous" })); + expect(resultKeys(s)).toEqual(firstPage); // A new query starts again from the first page. fireEvent.change(searchInput(s), { target: { value: "dog" } }); @@ -1634,6 +1641,29 @@ describe("DefinitionsTab Font Awesome search", () => { expect(within(s).getByRole("radiogroup", { name: "Font Awesome" })).toBeTruthy(); }); + it("paging through every match never mounts more than one page", async () => { + await renderReady(); + fireEvent.click(screen.getByRole("button", { name: "New" })); + const s = sheet(); + fireEvent.change(searchInput(s), { target: { value: "-" } }); + const all = searchFaIcons("-").map((icon) => `fa:${icon.name}`); + expect(all.length).toBeGreaterThan(900); + + const seen: string[] = []; + for (;;) { + expect(results(s).children.length).toBeGreaterThan(0); + expect(results(s).children.length).toBeLessThanOrEqual(40); + seen.push(...resultKeys(s)); + const more = within(s).queryByRole("button", { name: "Show more" }); + if (!more) break; + fireEvent.click(more); + } + // Every match stays reachable, one page at a time, in order. + expect(seen).toEqual(all); + const lastStart = Math.floor((all.length - 1) / 40) * 40 + 1; + expect(within(s).getByText(`Showing ${lastStart}–${all.length} of ${all.length}`)).toBeTruthy(); + }); + it("selecting a search result POSTs fa: and renders on its row", async () => { await renderReady(); fireEvent.click(screen.getByRole("button", { name: "New" })); diff --git a/admin/src/definitions/DefinitionsTab.tsx b/admin/src/definitions/DefinitionsTab.tsx index 8a00871..81282e6 100644 --- a/admin/src/definitions/DefinitionsTab.tsx +++ b/admin/src/definitions/DefinitionsTab.tsx @@ -92,7 +92,7 @@ const REPEATS_BY_RULE: Record = { /** How many upcoming dates the edit sheet previews. */ const PREVIEW_COUNT = 5; -/** Font Awesome search results shown at once, and added per "Show more". */ +/** Font Awesome search results per page; only one page is ever mounted. */ const FA_RESULTS_PAGE = 40; /** Quiet period after the last rule edit before the preview is requested. */ @@ -498,7 +498,9 @@ function EditSheet({ : target.assignees.filter((a) => !activeChildren.some((child) => child.id === a.id)); const [pickerOpen, setPickerOpen] = useState(false); const faResults = useMemo(() => searchFaIcons(draft.faText), [draft.faText]); - const [faShown, setFaShown] = useState(FA_RESULTS_PAGE); + const [faPage, setFaPage] = useState(0); + const faStart = faPage * FA_RESULTS_PAGE; + const faEnd = Math.min(faStart + FA_RESULTS_PAGE, faResults.length); const noIcon = draft.icon === null && !draft.emoji && !draft.faText.trim(); const [saving, setSaving] = useState(false); const [confirming, setConfirming] = useState(false); @@ -676,7 +678,7 @@ function EditSheet({ placeholder="Search or type an icon name, e.g. dog" value={draft.faText} onChange={(e) => { - setFaShown(FA_RESULTS_PAGE); + setFaPage(0); update({ faText: e.target.value, icon: faTypedIconKey(e.target.value), emoji: "" }); }} /> @@ -693,7 +695,7 @@ function EditSheet({ role="radiogroup" aria-label="Font Awesome search results" > - {faResults.slice(0, faShown).map((icon) => { + {faResults.slice(faStart, faEnd).map((icon) => { const { name, label } = icon; const on = faIconName(draft.icon) === name; return ( @@ -716,16 +718,21 @@ function EditSheet({ })}

- Showing {Math.min(faShown, faResults.length)} of {faResults.length} + Showing {faStart + 1}–{faEnd} of {faResults.length}

- {faResults.length > faShown ? ( - + {faResults.length > FA_RESULTS_PAGE ? ( +
+ {faPage > 0 ? ( + + ) : null} + {faEnd < faResults.length ? ( + + ) : null} +
) : null} ) : null