From 8473ed339356b5187c151457296f18500a0d933f Mon Sep 17 00:00:00 2001 From: ZDDC Date: Mon, 15 Jun 2026 09:47:11 -0500 Subject: [PATCH] feat(classifier): Folder Tree count badge reflects the active filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-folder "direct+total folders / files" badge always showed the raw scanned totals, even while a filter narrowed the tree — so "9+1799 folders, 0+6203 files" stayed put no matter what you filtered to. computeVisible already does a single filtered pass (applying the Show checkboxes via classifyAllows and the autofilter via nameHit); accumulate per-folder visible direct/total counts there and have populateCount use them whenever a filter is active (raw totals otherwise). So the badge now shows the post-filter totals for both the autofilter and the Show checkboxes — a collapsed folder's badge tells you how many matching items are inside. Test: a filtered tree's root badge drops from "2 folders, 0+3 files" to "1 folder, 0+1 file". Classifier suites 69 green. Co-Authored-By: Claude Opus 4.8 (1M context) --- classifier/js/tree.js | 25 ++++++++++++++++++------- tests/classify.spec.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/classifier/js/tree.js b/classifier/js/tree.js index ea6bd17..b24b509 100644 --- a/classifier/js/tree.js +++ b/classifier/js/tree.js @@ -111,33 +111,39 @@ var visible = null; // { folders, files } while filtering, else null function computeVisible() { var c = window.app.modules.classify; - var folders = Object.create(null), files = Object.create(null); + var folders = Object.create(null), files = Object.create(null), counts = Object.create(null); var nf = filterActive(); function walk(folder, ancMatched) { var selfMatch = nf && nameHit(folder.path || folder.name); var matched = ancMatched || selfMatch; var show = false, hasFile = false, descMatch = false; + // Post-filter counts for the row's "direct+total" badge: direct = + // immediate visible children/files, total = visible across the subtree. + var dDir = 0, tDir = 0, dFile = 0, tFile = 0; (folder.children || []).forEach(function (ch) { var r = walk(ch, matched); - if (r.show) show = true; + if (r.show) { show = true; dDir++; tDir += 1 + r.tDir; } if (r.hasFile) hasFile = true; if (r.subtreeMatch) descMatch = true; // a child leads to a match + tFile += r.tFile; }); (folder.files || []).forEach(function (f) { hasFile = true; if (!classifyAllows(f)) return; var fileMatch = nf && nameHit(c.srcKeyForFile(f)); - if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; } + if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; dFile++; } if (fileMatch) descMatch = true; // a match sits directly in this folder }); + tFile += dFile; if (matched) show = true; // "Show Empty" off → hide folders whose whole subtree holds no files. if (!hasFile && !showEmpty && !matched) show = false; if (show) folders[folder.path] = true; - return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch }; + counts[folder.path] = { dDir: dDir, tDir: tDir, dFile: dFile, tFile: tFile }; + return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch, tDir: tDir, tFile: tFile }; } (window.app.folderTree || []).forEach(function (root) { walk(root, false); }); - return { folders: folders, files: files }; + return { folders: folders, files: files, counts: counts }; } function folderShown(folder) { return !visible || !!visible.folders[folder.path]; } function fileShown(file) { @@ -211,8 +217,13 @@ const done = st === 'done'; // When fully scanned both numbers are blue; .done turns the labels blue too. if (done) el.classList.add('done'); - const dDir = folder.subdirCount || 0, tDir = folder.runDirs || 0; - const dFile = folder.fileCount || 0, tFile = folder.runFiles || 0; + // While a filter (autofilter or a Show checkbox) is narrowing the tree, + // the badge counts what's VISIBLE; otherwise the raw scanned totals. + const vc = visible && visible.counts && visible.counts[folder.path]; + const dDir = vc ? vc.dDir : (folder.subdirCount || 0); + const tDir = vc ? vc.tDir : (folder.runDirs || 0); + const dFile = vc ? vc.dFile : (folder.fileCount || 0); + const tFile = vc ? vc.tFile : (folder.runFiles || 0); const frag = document.createDocumentFragment(); frag.appendChild(document.createTextNode('(')); diff --git a/tests/classify.spec.js b/tests/classify.spec.js index 9bad2cd..073703e 100644 --- a/tests/classify.spec.js +++ b/tests/classify.spec.js @@ -849,6 +849,35 @@ test('filter does not open collapsed branches; non-matching siblings hide', asyn expect(r.files).toEqual([]); }); +test('folder count badge shows post-filter totals', async ({ page }) => { + await page.click('#modeClassifyBtn'); + const r = await page.evaluate(() => { + window.app.folderTree = [{ + name: 'Root', path: 'Root', expanded: true, scanState: 'done', + subdirCount: 2, runDirs: 2, fileCount: 0, runFiles: 3, files: [], children: [ + { name: 'A', path: 'Root/A', expanded: true, scanState: 'done', subdirCount: 0, runDirs: 0, fileCount: 2, runFiles: 2, children: [], files: [ + { originalFilename: 'alpha report', extension: 'pdf', folderPath: 'Root/A' }, + { originalFilename: 'beta memo', extension: 'pdf', folderPath: 'Root/A' }, + ] }, + { name: 'B', path: 'Root/B', expanded: true, scanState: 'done', subdirCount: 0, runDirs: 0, fileCount: 1, runFiles: 1, children: [], files: [ + { originalFilename: 'gamma note', extension: 'pdf', folderPath: 'Root/B' }, + ] }, + ], + }]; + const tree = window.app.modules.tree; + const rootCount = () => { const e = document.querySelector('#folderTree .folder-item .folder-count'); return e ? e.textContent : null; }; + tree.render(); + const before = rootCount(); // no filter → raw scan totals + tree.setNameFilter('alpha'); // matches one file, in folder A only + const after = rootCount(); + return { before, after }; + }); + expect(r.before).toContain('2 folders'); // raw: 2 subfolders… + expect(r.before).toContain('0+3 files'); // …3 files in the subtree + expect(r.after).toContain('1 folder'); // filtered: only A is visible + expect(r.after).toContain('0+1 file'); // …holding the single matching file +}); + test('snapshot: a scanned zip subtree round-trips with its virtual members', async ({ page }) => { const r = await page.evaluate(() => { const sc = window.app.modules.scanner;