feat(classifier): Show Empty toggle; tidy the folder-tree header
- Add a "Show Empty" checkbox (classify mode) — when off, folders whose whole subtree contains no files are hidden, decluttering messy scans. - Move the Show Unassigned/Assigned/Excluded/Empty filters out of the cramped pane header into a dedicated "Show …" toolbar row beneath it (wraps cleanly). - Drop the "X folders selected" text from the folder-tree header (selection still works; updateSelectedCount guards the now-absent element). Test: Show Empty off hides file-less folders (classify.spec.js -> 41). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c61cac7c8f
commit
aa34a4b3e7
5 changed files with 70 additions and 28 deletions
|
|
@ -76,6 +76,8 @@
|
|||
}
|
||||
|
||||
.folder-tree-pane.collapsed .pane-header-controls,
|
||||
.folder-tree-pane.collapsed .classify-filters,
|
||||
.folder-tree-pane.collapsed .tree-filter,
|
||||
.folder-tree-pane.collapsed .folder-tree,
|
||||
.folder-tree-pane.collapsed .pane-header h3 {
|
||||
display: none;
|
||||
|
|
@ -141,13 +143,19 @@
|
|||
|
||||
.pane-header-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem 0.75rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Classify-mode source-tree filters (Show Unassigned/Assigned/Excluded). */
|
||||
.classify-filters { display: inline-flex; flex-wrap: wrap; gap: 0.3rem 0.75rem; justify-content: flex-end; }
|
||||
/* Classify-mode filter row, laid out as a toolbar under the pane header. */
|
||||
.tree-toolbar {
|
||||
display: flex; flex-wrap: wrap; align-items: center;
|
||||
gap: 0.2rem 0.7rem; padding: 0.3rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.tree-toolbar__label { color: var(--text-muted); font-size: 0.8rem; font-weight: 600; }
|
||||
.classify-filters .filter-count { color: var(--text-muted); font-size: 0.85em; }
|
||||
|
||||
/* Live filter box above a file tree. */
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@
|
|||
showUnassignedCheckbox: document.getElementById('showUnassignedCheckbox'),
|
||||
showAssignedCheckbox: document.getElementById('showAssignedCheckbox'),
|
||||
showExcludedCheckbox: document.getElementById('showExcludedCheckbox'),
|
||||
showEmptyCheckbox: document.getElementById('showEmptyCheckbox'),
|
||||
exportDatasetBtn: document.getElementById('exportDatasetBtn'),
|
||||
importDatasetBtn: document.getElementById('importDatasetBtn'),
|
||||
importDatasetInput: document.getElementById('importDatasetInput'),
|
||||
|
|
@ -336,10 +337,11 @@
|
|||
unassigned: app.dom.showUnassignedCheckbox.checked,
|
||||
assigned: app.dom.showAssignedCheckbox.checked,
|
||||
excluded: app.dom.showExcludedCheckbox.checked,
|
||||
empty: app.dom.showEmptyCheckbox.checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
[app.dom.showUnassignedCheckbox, app.dom.showAssignedCheckbox, app.dom.showExcludedCheckbox]
|
||||
[app.dom.showUnassignedCheckbox, app.dom.showAssignedCheckbox, app.dom.showExcludedCheckbox, app.dom.showEmptyCheckbox]
|
||||
.forEach(function (cb) { if (cb) cb.addEventListener('change', pushClassifyFilters); });
|
||||
|
||||
// Collapse tree button
|
||||
|
|
|
|||
|
|
@ -42,12 +42,14 @@
|
|||
// (so unchecking Assigned+Excluded leaves only what's left to do). A folder
|
||||
// whose whole scanned subtree is filtered away is itself hidden.
|
||||
var showFilters = { unassigned: true, assigned: true, excluded: true };
|
||||
var showEmpty = true; // show folders that contain no files
|
||||
function setShowFilters(f) {
|
||||
showFilters = {
|
||||
unassigned: f.unassigned !== false,
|
||||
assigned: f.assigned !== false,
|
||||
excluded: f.excluded !== false,
|
||||
};
|
||||
showEmpty = f.empty !== false;
|
||||
render();
|
||||
}
|
||||
function allFiltersOn() { return showFilters.unassigned && showFilters.assigned && showFilters.excluded; }
|
||||
|
|
@ -81,8 +83,8 @@
|
|||
for (var i = 0; i < filterTerms.length; i++) { if (t.indexOf(filterTerms[i]) === -1) return false; }
|
||||
return true;
|
||||
}
|
||||
// Anything narrowing the tree (a name search, or a show-filter turned off).
|
||||
function anyFilter() { return filterActive() || (classifyOn() && !allFiltersOn()); }
|
||||
// Anything narrowing the tree (a name search, a show-filter off, or hiding empties).
|
||||
function anyFilter() { return filterActive() || (classifyOn() && (!allFiltersOn() || !showEmpty)); }
|
||||
// One pass → the set of folder paths + file keys to render. A file shows when
|
||||
// it passes the show-filters AND (no name search, OR an ancestor folder
|
||||
// matched, OR its own path/name matches). A folder shows when it (or an
|
||||
|
|
@ -95,15 +97,22 @@
|
|||
var nf = filterActive();
|
||||
function walk(folder, ancMatched) {
|
||||
var matched = ancMatched || (nf && nameHit(folder.path || folder.name));
|
||||
var show = false;
|
||||
(folder.children || []).forEach(function (ch) { if (walk(ch, matched)) show = true; });
|
||||
var show = false, hasFile = false;
|
||||
(folder.children || []).forEach(function (ch) {
|
||||
var r = walk(ch, matched);
|
||||
if (r.show) show = true;
|
||||
if (r.hasFile) hasFile = true;
|
||||
});
|
||||
(folder.files || []).forEach(function (f) {
|
||||
hasFile = true;
|
||||
if (!classifyAllows(f)) return;
|
||||
if (!nf || matched || nameHit(c.srcKeyForFile(f))) { files[c.srcKeyForFile(f)] = true; show = true; }
|
||||
});
|
||||
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;
|
||||
return { show: show, hasFile: hasFile };
|
||||
}
|
||||
(window.app.folderTree || []).forEach(function (root) { walk(root, false); });
|
||||
return { folders: folders, files: files };
|
||||
|
|
@ -610,9 +619,10 @@
|
|||
* Update selected folders count
|
||||
*/
|
||||
function updateSelectedCount() {
|
||||
const el = window.app.dom.selectedFoldersCount;
|
||||
if (!el) return; // count no longer shown in the folder-tree header
|
||||
const count = window.app.selectedFolders.size;
|
||||
window.app.dom.selectedFoldersCount.textContent =
|
||||
`${count} folder${count !== 1 ? 's' : ''} selected`;
|
||||
el.textContent = `${count} folder${count !== 1 ? 's' : ''} selected`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -61,23 +61,27 @@
|
|||
<input type="checkbox" id="hideCompliantCheckbox">
|
||||
Hide Compliant
|
||||
</label>
|
||||
<span id="classifyFilters" class="classify-filters" hidden>
|
||||
<label class="checkbox-label" title="Show files not yet assigned in the active tab">
|
||||
<input type="checkbox" id="showUnassignedCheckbox" checked>
|
||||
Show Unassigned <span class="filter-count" id="showUnassignedCount"></span>
|
||||
</label>
|
||||
<label class="checkbox-label" title="Show files already assigned in the active tab">
|
||||
<input type="checkbox" id="showAssignedCheckbox" checked>
|
||||
Show Assigned <span class="filter-count" id="showAssignedCount"></span>
|
||||
</label>
|
||||
<label class="checkbox-label" title="Show excluded files">
|
||||
<input type="checkbox" id="showExcludedCheckbox" checked>
|
||||
Show Excluded <span class="filter-count" id="showExcludedCount"></span>
|
||||
</label>
|
||||
</span>
|
||||
<span id="selectedFoldersCount" class="folder-count">0 folders selected</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="classifyFilters" class="classify-filters tree-toolbar" hidden>
|
||||
<span class="tree-toolbar__label">Show</span>
|
||||
<label class="checkbox-label" title="Files not yet assigned in the active tab">
|
||||
<input type="checkbox" id="showUnassignedCheckbox" checked>
|
||||
Unassigned <span class="filter-count" id="showUnassignedCount"></span>
|
||||
</label>
|
||||
<label class="checkbox-label" title="Files already assigned in the active tab">
|
||||
<input type="checkbox" id="showAssignedCheckbox" checked>
|
||||
Assigned <span class="filter-count" id="showAssignedCount"></span>
|
||||
</label>
|
||||
<label class="checkbox-label" title="Excluded files">
|
||||
<input type="checkbox" id="showExcludedCheckbox" checked>
|
||||
Excluded <span class="filter-count" id="showExcludedCount"></span>
|
||||
</label>
|
||||
<label class="checkbox-label" title="Folders that contain no files">
|
||||
<input type="checkbox" id="showEmptyCheckbox" checked>
|
||||
Empty
|
||||
</label>
|
||||
</div>
|
||||
<input type="search" id="treeFilterInput" class="tree-filter" spellcheck="false"
|
||||
placeholder="Filter files… (e.g. master deliverables list)" aria-label="Filter files">
|
||||
<div id="folderTree" class="folder-tree">
|
||||
|
|
|
|||
|
|
@ -742,3 +742,21 @@ test('tracking-tree filter reveals matching nodes and hides the rest', async ({
|
|||
expect(names).toContain('0001');
|
||||
expect(names).not.toContain('XYZ');
|
||||
});
|
||||
|
||||
test('Show Empty off hides folders that contain no files', async ({ page }) => {
|
||||
await page.click('#modeClassifyBtn');
|
||||
const r = await page.evaluate(() => {
|
||||
window.app.folderTree = [
|
||||
{ name: 'Docs', path: 'Docs', expanded: true, scanState: 'done', children: [],
|
||||
files: [{ originalFilename: 'a', extension: 'pdf', folderPath: 'Docs' }] },
|
||||
{ name: 'EmptyDir', path: 'EmptyDir', expanded: true, scanState: 'done', children: [], files: [] },
|
||||
];
|
||||
window.app.modules.tree.render();
|
||||
const before = Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path).sort();
|
||||
window.app.modules.tree.setShowFilters({ unassigned: true, assigned: true, excluded: true, empty: false });
|
||||
const after = Array.from(document.querySelectorAll('#folderTree .folder-item')).map((e) => e.dataset.path).sort();
|
||||
return { before, after };
|
||||
});
|
||||
expect(r.before).toEqual(['Docs', 'EmptyDir']); // both shown by default
|
||||
expect(r.after).toEqual(['Docs']); // empty folder hidden when Show Empty off
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue