diff --git a/classifier/css/layout.css b/classifier/css/layout.css index 7845c7a..598cbab 100644 --- a/classifier/css/layout.css +++ b/classifier/css/layout.css @@ -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. */ diff --git a/classifier/js/app.js b/classifier/js/app.js index 3979c78..6d45c4e 100644 --- a/classifier/js/app.js +++ b/classifier/js/app.js @@ -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 diff --git a/classifier/js/tree.js b/classifier/js/tree.js index dcc6905..d19dbae 100644 --- a/classifier/js/tree.js +++ b/classifier/js/tree.js @@ -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`; } /** diff --git a/classifier/template.html b/classifier/template.html index ea3404e..e755d63 100644 --- a/classifier/template.html +++ b/classifier/template.html @@ -61,23 +61,27 @@ Hide Compliant - - - - - - 0 folders selected +