refactor(sort): source-agnostic SortControl + SortOption model (R13 S1 foundation)
Introduce the one sort control every content surface will share: - columnSort.ts gains the SortOption vocabulary + pure pickSort/healSort/ hydrateSort helpers (direction lives in SortState, never in the key). - SortControl.tsx: the generic dropdown + asc/desc toggle, with an optional "no sort" choice (tables) and a trailing adornment slot (the feed's reshuffle). - ColumnSortControl is now a thin wrapper over SortControl (API unchanged, so Channels + ChannelDiscovery are untouched); optionsFromColumns moved to lib/sortOptions.ts to keep the component file fast-refresh clean. - useSortState.ts: the per-account persist/hydrate/heal plumbing the channel manager and discovery each hand-rolled, ready to replace both. Pure logic is fully unit-tested (columnSort.test.ts 10 -> 23). Gate green: tsc/eslint/prettier clean, vitest 93 pass. Feed/Plex/channels migration follows.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { type Column } from "./DataTable";
|
||||
import { type SortState } from "../lib/columnSort";
|
||||
import { optionsFromColumns } from "../lib/sortOptions";
|
||||
import SortControl from "./SortControl";
|
||||
|
||||
// A sort control for views without clickable column headers (the channel manager's card layout):
|
||||
// a dropdown of the sortable columns + an asc/desc toggle, mirroring the feed toolbar's sort. It's
|
||||
// driven by the SAME `SortState` the DataTable header uses, so table and cards stay in sync. The
|
||||
// dropdown's options come straight from the columns' `sortable`/`sortValue` metadata — no new list.
|
||||
// The headerless sort control for views without clickable column headers (the channel manager's and
|
||||
// discovery's card layouts). Now a thin adapter over the generic `SortControl`, feeding it the
|
||||
// column-derived options and a "no sort" default — kept as its own component so the existing call
|
||||
// sites stay untouched and table/cards keep sharing one `SortState`.
|
||||
export default function ColumnSortControl<T>({
|
||||
columns,
|
||||
sort,
|
||||
@@ -25,44 +26,15 @@ export default function ColumnSortControl<T>({
|
||||
ascLabel: string;
|
||||
descLabel: string;
|
||||
}) {
|
||||
const sortable = columns.filter((c) => c.sortable && c.sortValue);
|
||||
// A stored sort can name a column this table no longer offers (renamed, or `sortable` dropped).
|
||||
// `sortRows` already leaves the rows unsorted then, so treat it as no sort here too — otherwise the
|
||||
// select falls back to showing "default" while a direction arrow claims a sort that isn't applied.
|
||||
const active = sortable.some((c) => c.key === sort?.key) ? sort : null;
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<select
|
||||
value={active?.key ?? ""}
|
||||
onChange={(e) => {
|
||||
const key = e.target.value;
|
||||
// Keep the direction when re-picking the same column; a new column starts ascending.
|
||||
onChange(key ? { key, dir: active?.key === key ? active.dir : "asc" } : null);
|
||||
}}
|
||||
aria-label={label}
|
||||
className="bg-surface border border-border rounded-lg px-2 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
|
||||
>
|
||||
<option value="">{defaultLabel}</option>
|
||||
{sortable.map((c) => (
|
||||
<option key={c.key} value={c.key}>
|
||||
{c.header}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{active && (
|
||||
<button
|
||||
onClick={() => onChange({ key: active.key, dir: active.dir === "asc" ? "desc" : "asc" })}
|
||||
title={active.dir === "asc" ? ascLabel : descLabel}
|
||||
aria-label={active.dir === "asc" ? ascLabel : descLabel}
|
||||
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
|
||||
>
|
||||
{active.dir === "asc" ? (
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
) : (
|
||||
<ArrowDown className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<SortControl
|
||||
options={optionsFromColumns(columns)}
|
||||
sort={sort}
|
||||
onChange={onChange}
|
||||
label={label}
|
||||
defaultLabel={defaultLabel}
|
||||
ascLabel={ascLabel}
|
||||
descLabel={descLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user