Files
siftlode/frontend/src/components/ColumnSortControl.tsx
T
peter be9d17ebb1 fix(managers): address the E4 S3 code-review findings
- DataTable: keep `pagerNode` NULLABLE. Extracting Pager made it an always-truthy
  element, so the `controls` / `bottomControls` guards always fired and a table with
  nothing to page rendered empty `my-3` bands (plus an empty in-band PageToolbar).
- ChannelsProvider: `setView` bails when the tab is unchanged (re-selecting the
  active tab, or goToFullHistory forcing "subscribed", stacked dead Back entries),
  and pushes a FRESH state like setPage does instead of spreading `_chan`/`_yt`
  markers into the tab entry.
- Lift the layout into its own narrow context (`useChannelLayout`): App read the
  whole ChannelsState for the fade, so every keystroke in the manager's search box
  re-rendered the app root.
- Reset the card page when the row set or sort changes (both tabs) — the clamp only
  hid a stale page, which came back when the filter was cleared.
- Sort + slice the card rows only in cards mode; table mode was copy-sorting ~300
  rows per render for a result nothing read.
- Validate persisted `cardSize` / `sort` (`parseCardSize`, `parseSortState`), matching
  the clamp-on-read convention the module's other persisted values already follow;
  a corrupt size made the page count NaN and emptied the grid.
- Pager: page-size changes scroll to the top too (they re-slice from a new first item).
- ColumnSortControl: a stored sort naming a column this table no longer offers is
  treated as no sort, instead of showing a direction arrow for a sort that isn't applied.
- Drop the stale "rows" layout comments (that variant was retired this sprint).
2026-07-20 00:34:38 +02:00

65 lines
2.6 KiB
TypeScript

import { ArrowDown, ArrowUp } from "lucide-react";
import { type Column } from "./DataTable";
import { type SortState } from "../lib/columnSort";
// 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.
export default function ColumnSortControl<T>({
columns,
sort,
onChange,
label,
defaultLabel,
ascLabel,
descLabel,
}: {
columns: Column<T>[];
sort: SortState;
onChange: (next: SortState) => void;
/** Accessible name for the dropdown (e.g. "Sort"). */
label: string;
/** The "no sort / original order" option's text. */
defaultLabel: string;
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-card border border-border rounded-lg px-2 py-1.5 text-sm outline-none focus:border-accent"
>
<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>
);
}