fix(sort): address R13 S1 review findings

- PlexSidebar activeCount now compares the HEALED sort, so an out-of-scope key
  (movie "duration" after switching to shows) no longer inflates the
  active-filter badge while the control + grid show the default.
- PlexProvider strips the legacy `sortDir` from the filters blob, so it stops
  leaking into the facets query key and being re-persisted on every change.
- PlexProvider sort migration guards the fall-through: valid JSON that isn't a
  SortState now falls back instead of using the raw JSON string as the key.
- New shared healPlexSort(sort, scope) is the single heal both PlexBrowse and
  PlexSidebar use (removes the noop-label plexSortOptions call + the duplication).
- Tests: useSortState.dom.test.ts (hydrate/persist/heal round-trip via renderHook)
  and SortControl.dom.test.tsx (options, pick, direction toggle, directionless,
  trailing). vitest 94 -> 105.

Gate green: tsc/eslint(0 err)/prettier/knip clean, vitest 105.
This commit is contained in:
2026-08-14 00:00:31 +02:00
parent 45bd588af7
commit 02c8c71c53
6 changed files with 164 additions and 15 deletions
+13 -2
View File
@@ -1,4 +1,4 @@
import { type SortOption, type SortState } from "./columnSort";
import { healSort, type SortOption, type SortState } from "./columnSort";
// Plex's sort vocabulary, source-agnostic like the feed's — a `SortOption[]` fed to the shared
// SortControl. Direction lives in the SortState (no more sibling `sortDir` on PlexFilters); the wire
@@ -16,9 +16,20 @@ function plexSortKeys(scope: string): string[] {
export const PLEX_SORT_FALLBACK: SortState = { key: "title", dir: "asc" };
/** Labelled, alpha-ordered options for the SortControl. `label` resolves each key (i18n
* `plex.sort.<key>`); pass a no-op label when only the key set matters (healing). */
* `plex.sort.<key>`). */
export function plexSortOptions(scope: string, label: (key: string) => string): SortOption[] {
return plexSortKeys(scope)
.map((key) => ({ key, label: label(key) }))
.sort((a, b) => a.label.localeCompare(b.label));
}
/** Heal a stored sort against a scope's available keys, collapsing an out-of-scope key to the default.
* The single heal both the sidebar control and the browse query use, so they never diverge. Always
* non-null (the fallback is), so callers can read `.key` without a guard. */
export function healPlexSort(sort: SortState, scope: string): NonNullable<SortState> {
return healSort(
sort,
plexSortKeys(scope).map((key) => ({ key, label: key })),
PLEX_SORT_FALLBACK,
)!;
}