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).
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, 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, readAccount, writeAccount } from "../lib/storage";
|
||||
import { formatCountOrDash } from "../lib/format";
|
||||
import { sortRows, type SortState } from "../lib/columnSort";
|
||||
import { parseSortState, sortRows, type SortState } from "../lib/columnSort";
|
||||
import { CARD_PAGE_SIZES, parseCardSize } from "../lib/channelLayout";
|
||||
import { subsColumn } from "./channelColumns";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||
@@ -18,7 +19,7 @@ import ColumnSortControl from "./ColumnSortControl";
|
||||
import Pager from "./Pager";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { useChannels } from "./ChannelsProvider";
|
||||
import { useChannelLayout } from "./ChannelsProvider";
|
||||
|
||||
// The Channel manager's "Discovery" tab: channels that turn up in the user's playlists but
|
||||
// that they don't subscribe to. Subscribing here only creates the subscription + enriches
|
||||
@@ -40,17 +41,24 @@ export default function ChannelDiscovery({
|
||||
const confirm = useConfirm();
|
||||
// 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 layout = useChannelLayout();
|
||||
const [sort, setSortState] = useState<SortState>(() =>
|
||||
readAccount<SortState>(LS.channelDiscoverySort, null),
|
||||
parseSortState(readAccount(LS.channelDiscoverySort, null)),
|
||||
);
|
||||
const setSort = (s: SortState) => {
|
||||
setSortState(s);
|
||||
writeAccount(LS.channelDiscoverySort, s);
|
||||
};
|
||||
// Card layout pagination (its own; the table keeps its rows-per-page inside DataTable).
|
||||
const [cardSize, setCardSize] = useState(() => readAccount<number>(LS.channelDiscoveryCardSize, 25));
|
||||
const [cardSize, setCardSize] = useState(() =>
|
||||
parseCardSize(readAccount(LS.channelDiscoveryCardSize, null)),
|
||||
);
|
||||
const [cardPage, setCardPage] = useState(0);
|
||||
// A narrowed or re-sorted list invalidates the page you were on — reset to the first, as the table
|
||||
// does on its own filter/sort changes.
|
||||
useEffect(() => {
|
||||
setCardPage(0);
|
||||
}, [search, sort]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["discovered-channels"],
|
||||
@@ -186,12 +194,15 @@ export default function ChannelDiscovery({
|
||||
},
|
||||
];
|
||||
|
||||
// Card layout paginates like the table (slice the sorted rows; the Pager reports changes).
|
||||
const sortedRows = sortRows(rows, columns, sort);
|
||||
// Card layout paginates like the table (slice the sorted rows; the Pager reports changes). Only
|
||||
// computed for cards — DataTable sorts and slices internally, so doing it in table mode too would
|
||||
// copy+sort the whole list every render for a result nothing reads.
|
||||
const isCards = layout === "cards";
|
||||
const sortedRows = isCards ? sortRows(rows, columns, sort) : rows;
|
||||
const cardTotalPages = cardSize <= 0 ? 1 : Math.max(1, Math.ceil(sortedRows.length / cardSize));
|
||||
const cardSafePage = Math.min(cardPage, cardTotalPages - 1);
|
||||
const pagedRows =
|
||||
cardSize <= 0
|
||||
!isCards || cardSize <= 0
|
||||
? sortedRows
|
||||
: sortedRows.slice(cardSafePage * cardSize, cardSafePage * cardSize + cardSize);
|
||||
|
||||
@@ -208,7 +219,7 @@ export default function ChannelDiscovery({
|
||||
page={cardSafePage}
|
||||
size={cardSize}
|
||||
total={sortedRows.length}
|
||||
pageSizeOptions={[10, 25, 50, 100]}
|
||||
pageSizeOptions={CARD_PAGE_SIZES}
|
||||
onPageChange={setCardPage}
|
||||
onSizeChange={(s) => {
|
||||
setCardSize(s);
|
||||
|
||||
Reference in New Issue
Block a user