diff --git a/classifier/js/tree.js b/classifier/js/tree.js index ddd9342..ea6bd17 100644 --- a/classifier/js/tree.js +++ b/classifier/js/tree.js @@ -111,7 +111,7 @@ 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), open = Object.create(null); + var folders = Object.create(null), files = Object.create(null); var nf = filterActive(); function walk(folder, ancMatched) { var selfMatch = nf && nameHit(folder.path || folder.name); @@ -134,19 +134,11 @@ // "Show Empty" off → hide folders whose whole subtree holds no files. if (!hasFile && !showEmpty && !matched) show = false; if (show) folders[folder.path] = true; - // Auto-open ONLY the connector folders on the path down to a match — - // never the matched node itself. Terminal matches and everything - // off-path keep their real collapse state; the root's expand-all - // covers the rest. (Search reveals where hits are; it doesn't reshape - // the tree.) - if (nf && descMatch) open[folder.path] = true; return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch }; } (window.app.folderTree || []).forEach(function (root) { walk(root, false); }); - return { folders: folders, files: files, open: open }; + return { folders: folders, files: files }; } - // True only for folders the search needs opened to expose a hit beneath them. - function autoOpen(folder) { return !!(visible && visible.open && visible.open[folder.path]); } function folderShown(folder) { return !visible || !!visible.folders[folder.path]; } function fileShown(file) { if (!classifyAllows(file)) return false; @@ -302,7 +294,7 @@ // expandable so its files can be revealed and dragged. || (classifyOn() && folder.files && folder.files.length > 0); if (mightHaveChildren) { - toggle.textContent = (folder.expanded || autoOpen(folder)) ? '▼' : '▶'; + toggle.textContent = folder.expanded ? '▼' : '▶'; toggle.addEventListener('click', (e) => { e.stopPropagation(); const recursive = e.ctrlKey || e.metaKey; @@ -366,9 +358,12 @@ div.appendChild(item); - // Children — when expanded, or opened on the path to a search hit below. - // The Show toggles never force-expand; search opens only connector folders. - if ((folder.expanded || autoOpen(folder)) && folder.children && folder.children.length > 0) { + // Children render ONLY when the user has expanded this folder. The + // autofilter and Show toggles never change expand/collapse state — they + // hide/show rows in place. A collapsed folder stays collapsed even if it + // contains matches (it's still shown, so the user can open it); this lets + // you filter within one subtree without the rest expanding. + if (folder.expanded && folder.children && folder.children.length > 0) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'folder-children'; sortedFolders(folder.children).forEach(child => { @@ -379,9 +374,9 @@ div.appendChild(childrenDiv); } - // Classify mode: list this folder's own files (draggable leaves) when - // expanded (or opened to reveal a search hit), so they can be dropped. - if (classifyOn() && (folder.expanded || autoOpen(folder)) && folder.files && folder.files.length > 0) { + // Classify mode: list this folder's own files (draggable leaves) only + // when the user has expanded it (the filter never force-expands). + if (classifyOn() && folder.expanded && folder.files && folder.files.length > 0) { const filesDiv = document.createElement('div'); filesDiv.className = 'folder-children folder-files'; sortedFiles(folder.files).forEach(function (file) { diff --git a/tests/classify.spec.js b/tests/classify.spec.js index 2d3ad6f..9bad2cd 100644 --- a/tests/classify.spec.js +++ b/tests/classify.spec.js @@ -729,29 +729,35 @@ test('dataset (filename-based): import reconstruction rebuilds tracking + shared expect(r.excluded).toBe(true); }); -test('source-tree filter reveals matches with their folder hierarchy', async ({ page }) => { +test('source-tree filter hides non-matches in place; never changes expand state', async ({ page }) => { await page.click('#modeClassifyBtn'); const r = await page.evaluate(() => { window.app.folderTree = [{ - name: 'Project', path: 'Project', expanded: false, scanState: 'done', files: [], children: [ - { name: 'Electrical', path: 'Project/Electrical', expanded: false, scanState: 'done', children: [], files: [ + name: 'Project', path: 'Project', expanded: true, scanState: 'done', files: [], children: [ + // EXPANDED: its match shows in place, the non-match is hidden. + { name: 'Electrical', path: 'Project/Electrical', expanded: true, scanState: 'done', children: [], files: [ { originalFilename: 'Master Deliverables List', extension: 'xlsx', folderPath: 'Project/Electrical' }, { originalFilename: 'Switchgear Spec', extension: 'pdf', folderPath: 'Project/Electrical' }, ] }, + // COLLAPSED but ALSO holds a match — it must stay collapsed (shown + // as a row, file NOT revealed): the filter never auto-expands. { name: 'Civil', path: 'Project/Civil', expanded: false, scanState: 'done', children: [], files: [ - { originalFilename: 'Site Plan', extension: 'pdf', folderPath: 'Project/Civil' }, + { originalFilename: 'master deliverables draft', extension: 'pdf', folderPath: 'Project/Civil' }, ] }, ], }]; window.app.modules.tree.render(); window.app.modules.tree.setNameFilter('master deliverables'); + const civil = window.app.folderTree[0].children[1]; return { files: Array.from(document.querySelectorAll('#folderTree .file-item .file-name')).map((e) => e.textContent), - folders: Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path), + folders: Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path).sort(), + civilStillCollapsed: civil.expanded === false, }; }); - expect(r.files).toEqual(['Master Deliverables List.xlsx']); // only the match shown - expect(r.folders).toEqual(['Project', 'Project/Electrical']); // path revealed; Civil hidden + expect(r.files).toEqual(['Master Deliverables List.xlsx']); // expanded folder: match in place, Switchgear hidden + expect(r.folders).toEqual(['Project', 'Project/Civil', 'Project/Electrical']); // Civil shown (has a match) but collapsed + expect(r.civilStillCollapsed).toBe(true); // the filter did NOT expand it }); test('tracking-tree filter reveals matching nodes and hides the rest', async ({ page }) => { @@ -803,18 +809,19 @@ test('toggling a Show filter preserves collapse state (no force-expand)', async // A Show toggle must not expand the collapsed parent… tree.setShowFilters({ unassigned: true, assigned: true, excluded: true, empty: false }); const afterToggle = Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path); - // …whereas a name search still reveals the match by auto-expanding. + // …and neither does the name filter — it hides/shows in place, never expands. tree.setShowFilters({ unassigned: true, assigned: true, excluded: true, empty: true }); tree.setNameFilter('a.pdf'); const afterSearch = Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path); - return { collapsed, afterToggle, afterSearch }; + return { collapsed, afterToggle, afterSearch, parentCollapsed: window.app.folderTree[0].expanded === false }; }); expect(r.collapsed).toEqual(['Project']); // child hidden — parent collapsed expect(r.afterToggle).toEqual(['Project']); // Show toggle leaves it collapsed - expect(r.afterSearch).toEqual(['Project', 'Project/Sub']); // name search auto-expands to the match + expect(r.afterSearch).toEqual(['Project']); // name filter leaves it collapsed (no force-expand) + expect(r.parentCollapsed).toBe(true); // expand state untouched }); -test('search opens only the branch with a hit, leaving siblings collapsed', async ({ page }) => { +test('filter does not open collapsed branches; non-matching siblings hide', async ({ page }) => { await page.click('#modeClassifyBtn'); const r = await page.evaluate(() => { window.app.folderTree = [{ @@ -829,15 +836,17 @@ test('search opens only the branch with a hit, leaving siblings collapsed', asyn }]; const tree = window.app.modules.tree; tree.render(); - tree.setNameFilter('switchgear'); // a file deep in the Electrical branch + tree.setNameFilter('switchgear'); // a file deep in the (collapsed) Electrical branch return { folders: Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path), files: Array.from(document.querySelectorAll('#folderTree .file-item .file-name')).map((e) => e.textContent), }; }); - // Path to the hit opens; the unrelated Civil sibling is not force-opened (stays out). - expect(r.folders).toEqual(['Project', 'Project/Electrical']); - expect(r.files).toEqual(['Switchgear Spec.pdf']); + // Project contains a match so it's shown — but stays COLLAPSED, so Electrical + // isn't rendered and the hit isn't revealed (the user expands to reach it). + // Civil has no match and is hidden. + expect(r.folders).toEqual(['Project']); + expect(r.files).toEqual([]); }); test('snapshot: a scanned zip subtree round-trips with its virtual members', async ({ page }) => {