diff --git a/docker/frontend/scripts/library-check.mjs b/docker/frontend/scripts/library-check.mjs index 3dff333..7ea460e 100644 --- a/docker/frontend/scripts/library-check.mjs +++ b/docker/frontend/scripts/library-check.mjs @@ -373,6 +373,53 @@ step('a right click on the empty column offers a folder', blank.join(',') === 'l 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. +await page.click('[data-key="lib-node-CheckRoll/2026"]'); +await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 4, null, { + timeout: 10_000, + polling: 50, +}); +const deepRows = await page.$$eval('[data-key^="lib-node-"]', (els) => els.length); +const deepTiles = await page.$$eval(thumbs, (els) => els.length); +await page.click('[data-key="lib-root-name"]', { button: 'right' }); +await page.waitForSelector('[data-key="lib-menu"]', { timeout: 10_000 }); +const head = await page.$$eval('[data-key="lib-menu"] button', (els) => els.map((b) => b.dataset.key)); +step('a right click on the root name offers to fold the tree', head.join(',') === 'lib-menu-collapse', JSON.stringify(head)); +step( + 'the column name still reads as the folder it belongs to', + (await page.$eval('[data-key="lib-root-name"]', (e) => e.textContent)) === 'Roll A', + 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`, +); +// 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 // with. The right click on the empty column answered with a folder, not with a diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 8a7da6a..c814586 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -107,8 +107,9 @@ export function Library() { const [note, setNote] = useState(null); // The folder menu: what a right click opens, and where. No folder means the // right click landed on the empty part of the column, which is the way in for - // one more folder. - const [menu, setMenu] = useState<{ folder: LibraryFolder | null; x: number; y: number } | null>(null); + // one more folder; `root` means it landed on the column's own name — the head + // of the tree, whose one act is folding the whole roll. + 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. const grip = useRef<{ x: number; w: number } | null>(null); @@ -513,8 +514,18 @@ 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. */} - + 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. */} + { + e.preventDefault(); + setMenu({ folder: null, root: true, x: Math.min(e.clientX, window.innerWidth - 190), y: Math.min(e.clientY, window.innerHeight - 96) }); + }} + > {root?.label || root?.name} {rows.map((n) => { @@ -703,12 +714,28 @@ export function Library() { )} {/* The context menu: a folder gets what belongs to a folder rather than to a - frame, and the empty part of the column gets the folder that is missing. - A folder whose permission has lapsed gets the one thing that can be done - about it. */} + frame, the empty part of the column gets the folder that is missing, and + the column's own name gets the whole tree folded. A folder whose + permission has lapsed gets the one thing that can be done about it. */} {menu ? (
e.preventDefault()}> - {!menu.folder ? ( + {menu.root ? ( + // Every row that has something under it, the root's own included: 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. + + ) : !menu.folder ? (