fix(managers): two-way menu flip + extract useCardPager
- The tag menu's flip was one-way: once anchored above it stayed there, so a row scrolling toward the top dragged the menu off the screen. It now re-decides both ways each pass — prefer the side it is on (no flapping), leave it as soon as that side stops fitting and the other does. Verified: trigger at y=229 (no room above) flips back below, menuTop 253 = btnBottom 249 + 4, nothing off-screen. - Extract the card pagination into lib/useCardPager: size (persisted, validated), page, the out-of-range clamp-commit, the slice and the Pager props lived twice, and every review round had to fix both copies in lockstep. The two tabs now keep only what differs — which rows, which storage key, and when to reset. The clamp is gated on the card layout too, so it no longer rewrites card state while the table is the one on screen.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CARD_PAGE_SIZES, parseCardSize } from "./channelLayout";
|
||||
import { readAccount, writeAccount } from "./storage";
|
||||
|
||||
// Client-side pagination for the channel manager's card layout, owned once instead of twice. Both
|
||||
// manager tabs page an in-memory list the same way, and keeping two copies meant every fix landed
|
||||
// twice (and the day one copy was missed, the tabs would page differently). The caller keeps only
|
||||
// what genuinely differs: which rows, which storage key, and when to jump back to page one.
|
||||
//
|
||||
// The page size persists per account; the page itself is transient. `enabled` is the card layout
|
||||
// flag — in table mode DataTable does its own paging, so this does no work and holds still.
|
||||
export function useCardPager<T>(rows: T[], sizeKey: string, enabled: boolean) {
|
||||
const { t } = useTranslation();
|
||||
const [size, setSize] = useState(() => parseCardSize(readAccount(sizeKey, null)));
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const totalPages = size <= 0 ? 1 : Math.max(1, Math.ceil(rows.length / size));
|
||||
const safePage = Math.min(page, totalPages - 1);
|
||||
|
||||
// A list that shrank under you (an unsubscribe, a refetch) leaves `page` out of range: the clamp
|
||||
// above only HIDES that, so commit it or it springs back the moment the list grows again. Only
|
||||
// fires when the page you are on stopped existing, so it never costs you your place otherwise.
|
||||
useEffect(() => {
|
||||
if (enabled && page > totalPages - 1) setPage(totalPages - 1);
|
||||
}, [enabled, page, totalPages]);
|
||||
|
||||
const paged =
|
||||
!enabled || size <= 0 ? rows : rows.slice(safePage * size, safePage * size + size);
|
||||
|
||||
return {
|
||||
paged,
|
||||
/** Spread onto <Pager/>. */
|
||||
pagerProps: {
|
||||
page: safePage,
|
||||
size,
|
||||
total: rows.length,
|
||||
pageSizeOptions: CARD_PAGE_SIZES,
|
||||
onPageChange: setPage,
|
||||
onSizeChange: (s: number) => {
|
||||
setSize(s);
|
||||
writeAccount(sizeKey, s);
|
||||
setPage(0);
|
||||
},
|
||||
sizeLabel: t("channels.cardsPerPage"),
|
||||
},
|
||||
/** Call when the caller's own filters change — a different list starts at page one. */
|
||||
resetPage: useCallback(() => setPage(0), []),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user