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 .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 .folder-tree,
|
||||||
.folder-tree-pane.collapsed .pane-header h3 {
|
.folder-tree-pane.collapsed .pane-header h3 {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -141,13 +143,19 @@
|
||||||
|
|
||||||
.pane-header-controls {
|
.pane-header-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.3rem 0.75rem;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Classify-mode source-tree filters (Show Unassigned/Assigned/Excluded). */
|
/* Classify-mode filter row, laid out as a toolbar under the pane header. */
|
||||||
.classify-filters { display: inline-flex; flex-wrap: wrap; gap: 0.3rem 0.75rem; justify-content: flex-end; }
|
.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; }
|
.classify-filters .filter-count { color: var(--text-muted); font-size: 0.85em; }
|
||||||
|
|
||||||
/* Live filter box above a file tree. */
|
/* Live filter box above a file tree. */
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,7 @@
|
||||||
showUnassignedCheckbox: document.getElementById('showUnassignedCheckbox'),
|
showUnassignedCheckbox: document.getElementById('showUnassignedCheckbox'),
|
||||||
showAssignedCheckbox: document.getElementById('showAssignedCheckbox'),
|
showAssignedCheckbox: document.getElementById('showAssignedCheckbox'),
|
||||||
showExcludedCheckbox: document.getElementById('showExcludedCheckbox'),
|
showExcludedCheckbox: document.getElementById('showExcludedCheckbox'),
|
||||||
|
showEmptyCheckbox: document.getElementById('showEmptyCheckbox'),
|
||||||
exportDatasetBtn: document.getElementById('exportDatasetBtn'),
|
exportDatasetBtn: document.getElementById('exportDatasetBtn'),
|
||||||
importDatasetBtn: document.getElementById('importDatasetBtn'),
|
importDatasetBtn: document.getElementById('importDatasetBtn'),
|
||||||
importDatasetInput: document.getElementById('importDatasetInput'),
|
importDatasetInput: document.getElementById('importDatasetInput'),
|
||||||
|
|
@ -336,10 +337,11 @@
|
||||||
unassigned: app.dom.showUnassignedCheckbox.checked,
|
unassigned: app.dom.showUnassignedCheckbox.checked,
|
||||||
assigned: app.dom.showAssignedCheckbox.checked,
|
assigned: app.dom.showAssignedCheckbox.checked,
|
||||||
excluded: app.dom.showExcludedCheckbox.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); });
|
.forEach(function (cb) { if (cb) cb.addEventListener('change', pushClassifyFilters); });
|
||||||
|
|
||||||
// Collapse tree button
|
// Collapse tree button
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,14 @@
|
||||||
// (so unchecking Assigned+Excluded leaves only what's left to do). A folder
|
// (so unchecking Assigned+Excluded leaves only what's left to do). A folder
|
||||||
// whose whole scanned subtree is filtered away is itself hidden.
|
// whose whole scanned subtree is filtered away is itself hidden.
|
||||||
var showFilters = { unassigned: true, assigned: true, excluded: true };
|
var showFilters = { unassigned: true, assigned: true, excluded: true };
|
||||||
|
var showEmpty = true; // show folders that contain no files
|
||||||
function setShowFilters(f) {
|
function setShowFilters(f) {
|
||||||
showFilters = {
|
showFilters = {
|
||||||
unassigned: f.unassigned !== false,
|
unassigned: f.unassigned !== false,
|
||||||
assigned: f.assigned !== false,
|
assigned: f.assigned !== false,
|
||||||
excluded: f.excluded !== false,
|
excluded: f.excluded !== false,
|
||||||
};
|
};
|
||||||
|
showEmpty = f.empty !== false;
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
function allFiltersOn() { return showFilters.unassigned && showFilters.assigned && showFilters.excluded; }
|
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; }
|
for (var i = 0; i < filterTerms.length; i++) { if (t.indexOf(filterTerms[i]) === -1) return false; }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Anything narrowing the tree (a name search, or a show-filter turned off).
|
// Anything narrowing the tree (a name search, a show-filter off, or hiding empties).
|
||||||
function anyFilter() { return filterActive() || (classifyOn() && !allFiltersOn()); }
|
function anyFilter() { return filterActive() || (classifyOn() && (!allFiltersOn() || !showEmpty)); }
|
||||||
// One pass → the set of folder paths + file keys to render. A file shows when
|
// 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
|
// 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
|
// matched, OR its own path/name matches). A folder shows when it (or an
|
||||||
|
|
@ -95,15 +97,22 @@
|
||||||
var nf = filterActive();
|
var nf = filterActive();
|
||||||
function walk(folder, ancMatched) {
|
function walk(folder, ancMatched) {
|
||||||
var matched = ancMatched || (nf && nameHit(folder.path || folder.name));
|
var matched = ancMatched || (nf && nameHit(folder.path || folder.name));
|
||||||
var show = false;
|
var show = false, hasFile = false;
|
||||||
(folder.children || []).forEach(function (ch) { if (walk(ch, matched)) show = true; });
|
(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) {
|
(folder.files || []).forEach(function (f) {
|
||||||
|
hasFile = true;
|
||||||
if (!classifyAllows(f)) return;
|
if (!classifyAllows(f)) return;
|
||||||
if (!nf || matched || nameHit(c.srcKeyForFile(f))) { files[c.srcKeyForFile(f)] = true; show = true; }
|
if (!nf || matched || nameHit(c.srcKeyForFile(f))) { files[c.srcKeyForFile(f)] = true; show = true; }
|
||||||
});
|
});
|
||||||
if (matched) 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;
|
if (show) folders[folder.path] = true;
|
||||||
return show;
|
return { show: show, hasFile: hasFile };
|
||||||
}
|
}
|
||||||
(window.app.folderTree || []).forEach(function (root) { walk(root, false); });
|
(window.app.folderTree || []).forEach(function (root) { walk(root, false); });
|
||||||
return { folders: folders, files: files };
|
return { folders: folders, files: files };
|
||||||
|
|
@ -610,9 +619,10 @@
|
||||||
* Update selected folders count
|
* Update selected folders count
|
||||||
*/
|
*/
|
||||||
function updateSelectedCount() {
|
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;
|
const count = window.app.selectedFolders.size;
|
||||||
window.app.dom.selectedFoldersCount.textContent =
|
el.textContent = `${count} folder${count !== 1 ? 's' : ''} selected`;
|
||||||
`${count} folder${count !== 1 ? 's' : ''} selected`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -61,23 +61,27 @@
|
||||||
<input type="checkbox" id="hideCompliantCheckbox">
|
<input type="checkbox" id="hideCompliantCheckbox">
|
||||||
Hide Compliant
|
Hide Compliant
|
||||||
</label>
|
</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>
|
</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"
|
<input type="search" id="treeFilterInput" class="tree-filter" spellcheck="false"
|
||||||
placeholder="Filter files… (e.g. master deliverables list)" aria-label="Filter files">
|
placeholder="Filter files… (e.g. master deliverables list)" aria-label="Filter files">
|
||||||
<div id="folderTree" class="folder-tree">
|
<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).toContain('0001');
|
||||||
expect(names).not.toContain('XYZ');
|
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