feat(managers): shared sort across the table and card layouts

Cards have no column headers to sort by, so lift the sort out of DataTable into
one shared state that BOTH views drive and reflect. New lib/columnSort.ts holds
the comparator + click-cycle (one source of truth); DataTable gains an optional
controlled sort (sort + onSortChange) and falls back to its internal persistKey
sort when they're omitted, so every other table is untouched. The subscribed
sort lives in ChannelsProvider, discovery keeps its own (its columns differ);
a new ColumnSortControl dropdown (options derived from the columns' sortable
metadata, feed-style + asc/desc toggle) drives it in cards mode. Per-account,
persisted, per-tab.
This commit is contained in:
2026-07-19 23:09:54 +02:00
parent 875c7ad741
commit 9c4dd0163a
9 changed files with 195 additions and 31 deletions
+29 -4
View File
@@ -1,9 +1,11 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { UserPlus } from "lucide-react";
import { api, type DiscoveredChannel } from "../lib/api";
import { accountKey, LS } from "../lib/storage";
import { accountKey, LS, readAccount, writeAccount } from "../lib/storage";
import { formatCountOrDash } from "../lib/format";
import { sortRows, type SortState } from "../lib/columnSort";
import { subsColumn } from "./channelColumns";
import { notify } from "../lib/notifications";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
@@ -12,6 +14,7 @@ import ChannelLink from "./ChannelLink";
import AboutCell from "./AboutCell";
import DataTable, { type Column } from "./DataTable";
import ChannelLayoutGrid from "./ChannelLayoutGrid";
import ColumnSortControl from "./ColumnSortControl";
import { PageToolbar } from "./PageShell";
import { useConfirm } from "./ConfirmProvider";
import { useChannels } from "./ChannelsProvider";
@@ -34,8 +37,16 @@ export default function ChannelDiscovery({
const { t } = useTranslation();
const qc = useQueryClient();
const confirm = useConfirm();
// The table/cards/rows density is shared with the Subscriptions tab (ChannelsProvider).
// The table/cards density is shared with the Subscriptions tab (ChannelsProvider). The sort is
// this tab's own (its columns differ), lifted out of the table so the card layout shares it.
const { layout } = useChannels();
const [sort, setSortState] = useState<SortState>(() =>
readAccount<SortState>(LS.channelDiscoverySort, null),
);
const setSort = (s: SortState) => {
setSortState(s);
writeAccount(LS.channelDiscoverySort, s);
};
const query = useQuery({
queryKey: ["discovered-channels"],
@@ -173,8 +184,20 @@ export default function ChannelDiscovery({
<>
{/* Intro + the table controls are fixed chrome; only the table rows scroll (sticky header). */}
<PageToolbar>
<div className="px-4 pt-3 max-w-[96rem] mx-auto">
<div className="px-4 pt-3 max-w-[96rem] mx-auto flex items-start justify-between gap-4">
<p className="text-xs text-muted leading-relaxed">{t("channels.discovery.intro")}</p>
{/* Cards has no column headers, so a dropdown drives the sort (table uses the headers). */}
{layout === "cards" && (
<ColumnSortControl
columns={columns}
sort={sort}
onChange={setSort}
label={t("channels.sort.label")}
defaultLabel={t("channels.sort.default")}
ascLabel={t("channels.sort.asc")}
descLabel={t("channels.sort.desc")}
/>
)}
</div>
</PageToolbar>
<div className="px-4 pb-4 max-w-[96rem] mx-auto">
@@ -189,11 +212,13 @@ export default function ChannelDiscovery({
controlsPosition="both"
controlsInBand
controlsBandClassName="px-4 max-w-[96rem] mx-auto"
sort={sort}
onSortChange={setSort}
emptyText={t("channels.discovery.empty")}
/>
) : (
<ChannelLayoutGrid
rows={rows}
rows={sortRows(rows, columns, sort)}
columns={columns}
rowKey={(c) => c.id}
aboutKey="about"