From 15ce7098a01bf513df39b6dd833067c2c270958b Mon Sep 17 00:00:00 2001 From: ZDDC Date: Wed, 10 Jun 2026 13:02:12 -0500 Subject: [PATCH] fix(classifier): Show toggles preserve tree state; add Reset to raw input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Toggling Show Unassigned/Assigned/Excluded/Empty no longer force-expands the whole tree. Auto-expand is now reserved for the NAME search (where revealing a match means expanding to it); the Show toggles only hide/show, leaving your expand/collapse state untouched. Also preserve scrollTop across re-render so a toggle doesn't jump the view to the top. - Add a "Reset" button (danger-styled, beside Export/Import) that discards every classification — tracking + transmittal trees, assignments, excludes, title overrides — and returns to just the raw scanned input. Your files are never touched. Destructive + irreversible, so it confirms with an "Export first" warning and no-ops (info toast) when there's nothing to reset. Tests: Show toggle preserves collapse vs. name-search auto-expand (classify 42). Co-Authored-By: Claude Opus 4.8 (1M context) --- classifier/js/app.js | 20 ++++++++++++++++++++ classifier/js/tree.js | 15 ++++++++++----- classifier/template.html | 1 + tests/classify.spec.js | 26 ++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/classifier/js/app.js b/classifier/js/app.js index 6d45c4e..ea0760f 100644 --- a/classifier/js/app.js +++ b/classifier/js/app.js @@ -151,6 +151,7 @@ exportDatasetBtn: document.getElementById('exportDatasetBtn'), importDatasetBtn: document.getElementById('importDatasetBtn'), importDatasetInput: document.getElementById('importDatasetInput'), + resetDatasetBtn: document.getElementById('resetDatasetBtn'), treeFilterInput: document.getElementById('treeFilterInput'), trackingFilterInput: document.getElementById('trackingFilterInput'), transmittalFilterInput: document.getElementById('transmittalFilterInput'), @@ -305,6 +306,24 @@ reader.onerror = function () { window.zddc.toast('Import failed — could not read the file.', 'error'); }; reader.readAsText(file); } + // Reset to a clean state: keep the scanned source tree (the raw input), but + // discard every classification — trees, assignments, excludes, overrides. + // Destructive and irreversible, so warn and steer the user to Export first. + function resetDataset() { + var c = app.modules.classify; + var n = Object.keys(c.serialize().assignments || {}).length; + if (!n && !c.getTrackingTree().length && !c.getTransmittalTree().length) { + window.zddc.toast('Nothing to reset — already at the raw input.', 'info'); + return; + } + if (!confirm('Reset to a clean state?\n\nThis discards ALL classifications (' + + n + ' assigned file' + (n === 1 ? '' : 's') + ', plus the tracking and ' + + 'transmittal trees) and returns to just the raw scanned input. Your actual ' + + 'files are not touched.\n\nThis cannot be undone — Export first if you might ' + + 'need this data.')) return; + c.reset(); + window.zddc.toast('Reset to the raw scanned input.', 'success'); + } /** * Set up event listeners @@ -369,6 +388,7 @@ // Dataset export / import (round-trip the classification through a JSON file). if (app.dom.exportDatasetBtn) app.dom.exportDatasetBtn.addEventListener('click', exportDataset); if (app.dom.importDatasetBtn) app.dom.importDatasetBtn.addEventListener('click', function () { app.dom.importDatasetInput.click(); }); + if (app.dom.resetDatasetBtn) app.dom.resetDatasetBtn.addEventListener('click', resetDataset); if (app.dom.importDatasetInput) app.dom.importDatasetInput.addEventListener('change', function () { if (this.files && this.files[0]) importDataset(this.files[0]); this.value = ''; // allow re-importing the same file diff --git a/classifier/js/tree.js b/classifier/js/tree.js index d19dbae..561174d 100644 --- a/classifier/js/tree.js +++ b/classifier/js/tree.js @@ -143,6 +143,9 @@ */ function render() { const container = window.app.dom.folderTree; + // Preserve scroll across re-render — toggling a Show filter shouldn't + // jump the view back to the top. + const prevScroll = container.scrollTop; wireClassifyInteractions(); container.innerHTML = ''; updateFilterCounts(); @@ -164,6 +167,7 @@ } updateSelectedCount(); + container.scrollTop = prevScroll; } /** @@ -268,7 +272,7 @@ // expandable so its files can be revealed and dragged. || (classifyOn() && folder.files && folder.files.length > 0); if (mightHaveChildren) { - toggle.textContent = (folder.expanded || visible) ? '▼' : '▶'; + toggle.textContent = (folder.expanded || filterActive()) ? '▼' : '▶'; toggle.addEventListener('click', (e) => { e.stopPropagation(); const recursive = e.ctrlKey || e.metaKey; @@ -346,8 +350,9 @@ div.appendChild(item); - // Children — when expanded, or auto-expanded to reveal a filter match. - if ((folder.expanded || visible) && folder.children && folder.children.length > 0) { + // Children — when expanded, or auto-expanded to reveal a NAME-search + // match. The Show toggles only hide/show; they never force-expand. + if ((folder.expanded || filterActive()) && folder.children && folder.children.length > 0) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'folder-children'; folder.children.forEach(child => { @@ -359,8 +364,8 @@ } // Classify mode: list this folder's own files (draggable leaves) when - // expanded (or auto-expanded by a filter), so they can be dropped. - if (classifyOn() && (folder.expanded || visible) && folder.files && folder.files.length > 0) { + // expanded (or auto-expanded by a name search), so they can be dropped. + if (classifyOn() && (folder.expanded || filterActive()) && folder.files && folder.files.length > 0) { const filesDiv = document.createElement('div'); filesDiv.className = 'folder-children folder-files'; folder.files.forEach(function (file) { diff --git a/classifier/template.html b/classifier/template.html index e755d63..c44442f 100644 --- a/classifier/template.html +++ b/classifier/template.html @@ -165,6 +165,7 @@ + diff --git a/tests/classify.spec.js b/tests/classify.spec.js index d6446f5..0a91e39 100644 --- a/tests/classify.spec.js +++ b/tests/classify.spec.js @@ -760,3 +760,29 @@ test('Show Empty off hides folders that contain no files', async ({ page }) => { expect(r.before).toEqual(['Docs', 'EmptyDir']); // both shown by default expect(r.after).toEqual(['Docs']); // empty folder hidden when Show Empty off }); + +test('toggling a Show filter preserves collapse state (no force-expand)', 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: 'Sub', path: 'Project/Sub', expanded: false, scanState: 'done', children: [], + files: [{ originalFilename: 'a', extension: 'pdf', folderPath: 'Project/Sub' }] }, + ], + }]; + const tree = window.app.modules.tree; + tree.render(); + const collapsed = Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path); + // 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. + 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 }; + }); + 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 +});