- A library past the list's cap is searched and sorted on the server; the count says "the newest". - Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope). - A file dropped where the page takes none never reaches the browser (which would unload the app). - The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line; a text shows its first 5000 lines and says where the rest is. - Confirms name the file (no more "your downloads"); the asking menu rows end in "…". - The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly while only paused work waits, and forgets a finished file that was deleted. - An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top; the link box never replaces what was typed. - Honest states: folders that failed to load, a file no longer there, the storage figure; the rail says "nothing downloaded" only when there is nothing. - Focus after deleting the open folder and when the format form opens; the format name's label. - ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer. - Copy fixes, stale comments and the speculative hub key; a contract test for the list's parameters; e2e: the Files page and the paste intake.
76 lines
3.4 KiB
TypeScript
76 lines
3.4 KiB
TypeScript
import { useCallback, 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,
|
|
/** Follow a row the caller just moved: each new `seq` turns to the page holding the row `match`
|
|
* finds — the same contract as `DataTable`'s `reveal`, for the card paint of the same list. */
|
|
reveal?: { seq: number; match: (row: T) => boolean } | null,
|
|
/** The size picker's name — what the cards ARE (the channel manager's are "cards"). */
|
|
sizeLabel?: string,
|
|
/** Which list `rows` are: another one (a folder, a search, a sort) starts at page one. */
|
|
scope?: string,
|
|
) {
|
|
const { t } = useTranslation();
|
|
const [size, setSize] = useState(() => parseCardSize(readAccount(sizeKey, null)));
|
|
const [page, setPage] = useState(0);
|
|
const [scopeSeen, setScopeSeen] = useState(scope);
|
|
if (scope !== scopeSeen) {
|
|
setScopeSeen(scope);
|
|
setPage(0);
|
|
}
|
|
|
|
// During render against the last seq seen, as in DataTable (seeded so a mount never jumps).
|
|
const [revealSeen, setRevealSeen] = useState(reveal?.seq);
|
|
if (reveal && reveal.seq !== revealSeen) {
|
|
setRevealSeen(reveal.seq);
|
|
const at = rows.findIndex(reveal.match);
|
|
if (enabled && at >= 0 && size > 0) setPage(Math.floor(at / size));
|
|
}
|
|
|
|
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: `safePage`
|
|
// 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.
|
|
//
|
|
// Adjusted DURING RENDER, not from an effect: React re-runs this component immediately and throws
|
|
// the first pass away, so nothing is committed with an out-of-range page and there is no second
|
|
// paint. The condition is what makes that safe — the very write that triggers it also falsifies it.
|
|
if (enabled && page > totalPages - 1) setPage(totalPages - 1);
|
|
|
|
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: sizeLabel ?? t("channels.cardsPerPage"),
|
|
},
|
|
/** Call when the caller's own filters change — a different list starts at page one. */
|
|
resetPage: useCallback(() => setPage(0), []),
|
|
};
|
|
}
|