diff --git a/docker/frontend/scripts/library-check.mjs b/docker/frontend/scripts/library-check.mjs index b7b85ff..5b32a25 100644 --- a/docker/frontend/scripts/library-check.mjs +++ b/docker/frontend/scripts/library-check.mjs @@ -374,11 +374,12 @@ await page.keyboard.press('Escape'); step('escape closes the folder menu too', (await page.$('[data-key="lib-menu"]')) === null); // The head of the tree is the whole tree's control: a right click on the column's -// own name folds every row that has something under it, so a roll read to the -// bottom of its dates comes back to its own line. The frames in the strip are the -// strip's business and do not move with it. The subfolder opened above carries a -// fold of its own, so it is opened first: the claim is what ONE click does to -// every row at once, not to the one that was already shut. +// own name, or on the row of a folder picked at the top level, folds the folders +// nested below the top level. The top-level row itself stays open — one click on +// its name already shuts it, so the menu item would only repeat that click — and +// the frames in the strip are the strip's business and do not move with the rows. +// The subfolder opened above carries a fold of its own, so it is opened first: +// the claim is what ONE click does to every row at once, not to one already shut. await page.click('[data-key="lib-node-CheckRoll/2026"]'); await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 4, null, { timeout: 10_000, @@ -407,29 +408,21 @@ step( await page.getAttribute('[data-key="lib-root-name"]', 'title') ); await page.click('[data-key="lib-menu-collapse"]'); -await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 1, null, { - timeout: 10_000, - polling: 50, -}); -const flatTiles = await page.$$eval(thumbs, (els) => els.length); -const shutRoot = await page.getAttribute('[data-key="lib-node-CheckRoll"]', 'aria-expanded'); -// Opening the root back up is the ruler for the rest: only the rows the fold shut -// are still away, and the subfolder two deep is one of them. -await page.click('[data-key="lib-node-CheckRoll"]'); await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 3, null, { timeout: 10_000, polling: 50, }); -const revealed = await page.$$eval('[data-key^="lib-node-"]', (els) => els.map((e) => e.dataset.key)); -step( - 'and one click folds every row that has something under it', - deepRows === 4 && shutRoot === 'false' && flatTiles === deepTiles && revealed.join(',') === 'lib-node-CheckRoll,lib-node-CheckRoll/2026,lib-node-CheckRoll/Empty', - `${deepRows} rows → 1, ${revealed.length} back with the root opened, ${flatTiles} tiles kept`, +const flatTiles = await page.$$eval(thumbs, (els) => els.length); +const ended = await page.$$eval('[data-key^="lib-node-"]', (els) => + els.map((e) => `${e.dataset.key}:${e.getAttribute('aria-expanded') ?? '-'}`) +); +step( + 'and one click folds the subfolders, leaving the top-level row open', + deepRows === 4 && + flatTiles === deepTiles && + ended.join(' ') === 'lib-node-CheckRoll:true lib-node-CheckRoll/2026:false lib-node-CheckRoll/Empty:-', + `${deepRows} rows → ${ended.length}, ${ended.join(' ')}, ${flatTiles} tiles kept`, ); -// The open folder is the visit's, not the fold's, so it is put back where the -// sections below left it. -await page.click('[data-key="lib-node-CheckRoll/2026"]'); -await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, thumbs, { timeout: 30_000 }); // The divider drags the column, and what the visit leaves behind — the width the // column was left at and the folder that was open — is what the next visit opens diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index c121cc8..f352858 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -109,7 +109,7 @@ export function Library() { // right click landed on the empty part of the column, which is the way in for // one more folder; `root` means it landed on the head of a tree — the column's // own name, or the row of a folder picked at the top level — which carries - // folding every row that has something under it. + // folding the folders nested below the top level. const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; x: number; y: number } | null>(null); // Where the drag of the column divider started, and how wide the column was // then — the pointer only ever reports a position, not a distance moved. @@ -355,6 +355,11 @@ export function Library() { [nodes] ); + // The folders nested below a top-level row — what COLLAPSE ALL has to fold. A + // top-level row is not one of them: it is its own fold, since one click on its + // name shuts it, so the menu item would only repeat the row's own click. + const nested = useMemo(() => new Set([...parents].filter((key) => key.includes('/'))), [parents]); + // What the column draws: a folded row takes its whole subtree with it. const rows = useMemo( () => @@ -517,7 +522,7 @@ export function Library() { {/* The column is titled with the folder the tree belongs to, so a renamed roll is named where its frames are named. The name is the whole tree's handle as well: one right click here - folds every row that has something under it. */} + folds the folders nested below the top level. */} { if (!folder) return; e.preventDefault(); @@ -725,18 +730,18 @@ export function Library() { {menu ? (
e.preventDefault()}> {/* Fold-the-tree leads the menu of a top-level row, and stands alone on - the column's own name: every row that has something under it goes, - so a long roll comes back to one line per folder and the click that - folds a row still opens it again one at a time. */} + the column's own name: the folders nested below the top level go, + one line per folder, and no top-level row ever leaves the column — + the row itself is one click away and its click already shuts it. */} {menu.root ? (