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:
@@ -31,7 +31,12 @@ import ChannelLayoutGrid from "./ChannelLayoutGrid";
|
||||
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
||||
import ColumnSortControl from "./ColumnSortControl";
|
||||
import Pager from "./Pager";
|
||||
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
|
||||
import {
|
||||
CARD_PAGE_SIZES,
|
||||
CHANNEL_LAYOUTS,
|
||||
parseCardSize,
|
||||
type ChannelLayout,
|
||||
} from "../lib/channelLayout";
|
||||
import { sortRows } from "../lib/columnSort";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
import ChannelLink from "./ChannelLink";
|
||||
@@ -39,7 +44,7 @@ import AboutCell from "./AboutCell";
|
||||
import ChannelDiscovery from "./ChannelDiscovery";
|
||||
import TagManager from "./TagManager";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { useChannels, useChannelsActions } from "./ChannelsProvider";
|
||||
import { useChannelLayout, useChannels, useChannelsActions } from "./ChannelsProvider";
|
||||
import { useNavigationActions } from "./NavigationProvider";
|
||||
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
||||
import { useWizard } from "./WizardProvider";
|
||||
@@ -61,10 +66,6 @@ const LAYOUT_ICON: Record<ChannelLayout, typeof Table> = {
|
||||
cards: LayoutGrid,
|
||||
};
|
||||
|
||||
// Cards-per-page options (mirrors the DataTable default); the card grid gets a pager like the table.
|
||||
const CARD_PAGE_SIZES = [10, 25, 50, 100];
|
||||
const CARD_SIZE_DEFAULT = 25;
|
||||
|
||||
export default function Channels() {
|
||||
const me = useMe();
|
||||
const canWrite = me.can_write;
|
||||
@@ -78,12 +79,12 @@ export default function Channels() {
|
||||
search,
|
||||
statusFilter,
|
||||
view,
|
||||
layout,
|
||||
sort,
|
||||
filtersResetToken,
|
||||
focusName: focusChannelName,
|
||||
focusToken: focusChannelToken,
|
||||
} = useChannels();
|
||||
const layout = useChannelLayout();
|
||||
const { setSearch, setStatusFilter, setView, setLayout, setSort, focusChannel: onFocusChannel } =
|
||||
useChannelsActions();
|
||||
const { openChannel: onViewChannel, setPage } = useNavigationActions();
|
||||
@@ -217,8 +218,14 @@ export default function Channels() {
|
||||
const [tagFilter, setTagFilter] = useState<number[]>([]);
|
||||
// Card layout pagination (the table keeps its own inside DataTable). Size persists per account;
|
||||
// the page is transient.
|
||||
const [cardSize, setCardSize] = useState(() => readAccount<number>(LS.channelCardSize, CARD_SIZE_DEFAULT));
|
||||
const [cardSize, setCardSize] = useState(() => parseCardSize(readAccount(LS.channelCardSize, null)));
|
||||
const [cardPage, setCardPage] = useState(0);
|
||||
// A narrowed or re-sorted list invalidates the page you were on, so go back to the first — the way
|
||||
// DataTable resets its own page on a filter/sort change. Without this the clamp only hides a stale
|
||||
// page: clearing the filter again would silently return you to page 10 of the old list.
|
||||
useEffect(() => {
|
||||
setCardPage(0);
|
||||
}, [statusFilter, search, tagFilter, sort]);
|
||||
// A focus-channel intent (header "without full history" / tag manager) seeds the search box.
|
||||
useEffect(() => {
|
||||
if (focusChannelName) setSearch(focusChannelName);
|
||||
@@ -492,8 +499,8 @@ export default function Channels() {
|
||||
</div>
|
||||
);
|
||||
|
||||
// Table / cards / rows density — shared by the subscribed + discovery tables (blocked is a chip
|
||||
// list, no layout to switch). Rides the same generic ViewSwitcher the feed uses.
|
||||
// Table / cards density — shared by the subscribed + discovery tables (blocked is a chip list, no
|
||||
// layout to switch). Rides the same generic ViewSwitcher the feed uses.
|
||||
const layoutOptions: ViewOption<ChannelLayout>[] = CHANNEL_LAYOUTS.map((id) => ({
|
||||
id,
|
||||
label: t("channels.layout." + id),
|
||||
@@ -524,11 +531,14 @@ export default function Channels() {
|
||||
);
|
||||
// The card layout paginates like the table (a page of cards + a pager), rather than one long
|
||||
// infinite scroll. Slice the sorted rows; the Pager reports page/size changes.
|
||||
const sortedChannels = sortRows(visibleChannels, columns, sort);
|
||||
// Only computed for cards: in table mode DataTable sorts and slices internally, so doing it here
|
||||
// too would copy+sort the whole ~300-row array on every render for a result nothing reads.
|
||||
const isCards = layout === "cards";
|
||||
const sortedChannels = isCards ? sortRows(visibleChannels, columns, sort) : visibleChannels;
|
||||
const cardTotalPages = cardSize <= 0 ? 1 : Math.max(1, Math.ceil(sortedChannels.length / cardSize));
|
||||
const cardSafePage = Math.min(cardPage, cardTotalPages - 1);
|
||||
const pagedCards =
|
||||
cardSize <= 0
|
||||
!isCards || cardSize <= 0
|
||||
? sortedChannels
|
||||
: sortedChannels.slice(cardSafePage * cardSize, cardSafePage * cardSize + cardSize);
|
||||
const cardPager = (
|
||||
@@ -691,7 +701,8 @@ export default function Channels() {
|
||||
<div className="px-4 pb-4 max-w-[96rem] mx-auto">
|
||||
{/* Channel table — only the rows scroll; the header row is sticky, and the controls row
|
||||
(status chips + pager) rides in the fixed band right above it (controlsInBand). The
|
||||
card/row layouts drop the table + pager for a virtualized grid over the same rows. */}
|
||||
card layout swaps the table for a virtualized grid over the same rows (its pager and
|
||||
status chips ride in the fixed band above instead). */}
|
||||
{channelsQuery.isLoading ? (
|
||||
<div className="text-muted py-8">{t("channels.loading")}</div>
|
||||
) : layout === "table" ? (
|
||||
|
||||
Reference in New Issue
Block a user