From ccd7170a2020b8b1554b8584ee6e3a009e11eccc Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 14 Aug 2026 00:10:13 +0200 Subject: [PATCH] fix(sort): harden shared sort primitives (R13 S1 review round 2) - SortControl no longer renders a blank select when an always-sorted view (no defaultLabel) is handed a key its options don't offer: it falls back to the first option instead of value="" (which also tripped React's controlled warning). Robustness now lives in the primitive, not each caller. - PLEX_SORT_FALLBACK is Object.frozen: it's handed out by reference from healSort/healPlexSort and stored as provider state, so freezing turns any accidental in-place SortState mutation into a loud error instead of silently corrupting the shared fallback. - Test: SortControl falls back to the first option for an unavailable key. Gate green: tsc/eslint(0 err)/prettier/knip clean, vitest 106. --- frontend/src/components/SortControl.dom.test.tsx | 5 +++++ frontend/src/components/SortControl.tsx | 6 +++++- frontend/src/lib/plexSort.ts | 6 ++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/SortControl.dom.test.tsx b/frontend/src/components/SortControl.dom.test.tsx index 2459bb1f..2ff69e73 100644 --- a/frontend/src/components/SortControl.dom.test.tsx +++ b/frontend/src/components/SortControl.dom.test.tsx @@ -47,6 +47,11 @@ describe("SortControl", () => { expect(none.value).toBe(""); }); + it("falls back to the first option (not a blank select) for an unavailable key without defaultLabel", () => { + renderControl({ key: "ghost", dir: "asc" }); + expect((screen.getByRole("combobox") as HTMLSelectElement).value).toBe("date"); + }); + it("picking a new option fires onChange at that option's defaultDir", () => { const { onChange } = renderControl({ key: "title", dir: "asc" }); fireEvent.change(screen.getByRole("combobox"), { target: { value: "date" } }); diff --git a/frontend/src/components/SortControl.tsx b/frontend/src/components/SortControl.tsx index d4fd080f..cf3be5c6 100644 --- a/frontend/src/components/SortControl.tsx +++ b/frontend/src/components/SortControl.tsx @@ -36,10 +36,14 @@ export default function SortControl({ const active = sort && options.some((o) => o.key === sort.key) ? sort : null; const activeOpt = active ? options.find((o) => o.key === active.key) : undefined; const showArrow = !!activeOpt && !activeOpt.directionless; + // An always-sorted view (no `defaultLabel`) must still show a real option when the current key isn't + // offered (a stale/out-of-scope sort) — fall back to the first option rather than a blank select + // (which also trips React's controlled-value warning). Views with a "no sort" option keep it. + const selectValue = active?.key ?? (defaultLabel === undefined ? (options[0]?.key ?? "") : ""); return (