refactor(plex): unify Plex sort onto SortControl + a single SortState (R13 S1)
Plex sort was a bespoke chip-pill row plus a separate asc/desc chip pair, with
the key in PlexState.sort and the direction hidden in PlexFilters.sortDir. Now:
- sort is one SortState {key,dir}; sortDir is dropped from PlexFilters (and from
the facet-key destructure). The wire call still splits it into sort + sort_dir
at the server boundary, so the backend is untouched.
- The chip sort-row is replaced by the shared SortControl (select + arrow) — the
same idiom the feed and channel manager use. New lib/plexSort.ts owns the
scope-dependent vocabulary (movies add duration) + the title/asc fallback.
- PlexProvider migrates the old storage in place: a legacy bare-key plexSort
string plus the old filters.sortDir become the {key,dir} state, so an existing
sort preference survives the upgrade.
- PlexBrowse and PlexSidebar heal the stored sort against the current scope's
options, so a movie-only key (duration) left over after switching to shows
can't ask the server for an unavailable sort.
Gate green: tsc clean, eslint 0 errors, prettier clean, vitest 93.
This commit is contained in:
@@ -4,11 +4,14 @@ import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Layers, ListMusic, Plus, SlidersHorizontal, X } from "lucide-react";
|
||||
import { api, EMPTY_PLEX_FILTERS, plexFilterCount, type PlexFilters } from "../lib/api";
|
||||
import { healSort } from "../lib/columnSort";
|
||||
import { plexSortOptions, PLEX_SORT_FALLBACK } from "../lib/plexSort";
|
||||
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
|
||||
import { useDebounced } from "../lib/useDebounced";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import SidePanel from "./SidePanel";
|
||||
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
||||
import SortControl from "./SortControl";
|
||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||
|
||||
// The Plex module's left filter column — now the shared floating SidePanel with its filter groups
|
||||
@@ -24,8 +27,6 @@ type Props = {
|
||||
};
|
||||
|
||||
const SHOW_OPTS = ["all", "unwatched", "in_progress", "watched"] as const;
|
||||
const MOVIE_SORTS = ["added", "release", "year", "rating", "duration", "title"] as const;
|
||||
const SHOW_SORTS = ["added", "release", "year", "rating", "title"] as const;
|
||||
const RATING_STEPS = [5, 6, 7, 8, 9];
|
||||
const ADDED_OPTS = ["24h", "1w", "1m", "6m", "1y"] as const;
|
||||
const DURATION_BUCKETS: { key: string; min: number | null; max: number | null }[] = [
|
||||
@@ -95,7 +96,7 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
qc.invalidateQueries({ queryKey: qk.plexPlaylists() });
|
||||
onOpenPlaylist(pl.id);
|
||||
}
|
||||
const { sortDir: _sd, collectionTitle: _ct, ...facetKey } = filters;
|
||||
const { collectionTitle: _ct, ...facetKey } = filters;
|
||||
const facetsQ = useQuery({
|
||||
queryKey: qk.plexFacets(scope, show, facetKey),
|
||||
queryFn: () => api.plexFacets(scope, filters, show),
|
||||
@@ -114,7 +115,7 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
const collections = collectionsQ.data?.collections ?? [];
|
||||
|
||||
const fCount = plexFilterCount(filters);
|
||||
const activeCount = fCount + (show !== "all" ? 1 : 0) + (sort !== "title" ? 1 : 0);
|
||||
const activeCount = fCount + (show !== "all" ? 1 : 0) + (sort && sort.key !== "title" ? 1 : 0);
|
||||
const anyActive = activeCount > 0;
|
||||
|
||||
const patch = (p: Partial<PlexFilters>) => setFilters({ ...filters, ...p });
|
||||
@@ -123,12 +124,10 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
const clearAll = () => {
|
||||
setFilters(EMPTY_PLEX_FILTERS);
|
||||
setShow("all");
|
||||
setSort("title");
|
||||
setSort(PLEX_SORT_FALLBACK);
|
||||
};
|
||||
|
||||
const sorts = [...(scope === "movie" ? MOVIE_SORTS : SHOW_SORTS)].sort((a, b) =>
|
||||
t(`plex.sort.${a}`).localeCompare(t(`plex.sort.${b}`)),
|
||||
);
|
||||
const sortOptions = plexSortOptions(scope, (k) => t(`plex.sort.${k}`));
|
||||
const durBucketKey = DURATION_BUCKETS.find(
|
||||
(b) => (filters.durationMin ?? null) === b.min && (filters.durationMax ?? null) === b.max,
|
||||
)?.key;
|
||||
@@ -218,26 +217,14 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
||||
id: "sort",
|
||||
title: t("plex.filter.sort"),
|
||||
body: (
|
||||
<>
|
||||
<ChipRow>
|
||||
{sorts.map((s) => (
|
||||
<Chip key={s} active={sort === s} onClick={() => setSort(s)}>
|
||||
{t(`plex.sort.${s}`)}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipRow>
|
||||
<div className="mt-1.5 flex gap-1">
|
||||
{(["asc", "desc"] as const).map((d) => (
|
||||
<Chip
|
||||
key={d}
|
||||
active={(filters.sortDir ?? "asc") === d}
|
||||
onClick={() => patch({ sortDir: d })}
|
||||
>
|
||||
{t(`plex.filter.dir.${d}`)}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
<SortControl
|
||||
options={sortOptions}
|
||||
sort={healSort(sort, sortOptions, PLEX_SORT_FALLBACK)}
|
||||
onChange={(next) => setSort(next ?? PLEX_SORT_FALLBACK)}
|
||||
label={t("plex.filter.sort")}
|
||||
ascLabel={t("plex.filter.dir.asc")}
|
||||
descLabel={t("plex.filter.dir.desc")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(peopleActive
|
||||
|
||||
Reference in New Issue
Block a user