Compare commits
2 commits
60678e552d
...
7c158be73b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c158be73b | |||
| 0d8125a331 |
9 changed files with 61 additions and 62 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Archive</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||
|
|
|
|||
|
|
@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Browse</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
||||
|
|
|
|||
|
|
@ -2421,7 +2421,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Classifier</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||
|
|
@ -10083,7 +10083,7 @@ X.B(E,Y);return E}return J}())
|
|||
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);
|
||||
|
|
@ -10106,19 +10106,11 @@ X.B(E,Y);return E}return J}())
|
|||
// "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;
|
||||
|
|
@ -10274,7 +10266,7 @@ X.B(E,Y);return E}return J}())
|
|||
// 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;
|
||||
|
|
@ -10338,9 +10330,12 @@ X.B(E,Y);return E}return J}())
|
|||
|
||||
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 => {
|
||||
|
|
@ -10351,9 +10346,9 @@ X.B(E,Y);return E}return J}())
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -1793,7 +1793,7 @@ body {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
|
|||
|
|
@ -2770,7 +2770,7 @@ dialog.modal--narrow {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Transmittal</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
|
||||
</div>
|
||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||
<!-- Publish split-button (Transmittal-specific primary action;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||
archive=v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a
|
||||
transmittal=v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a
|
||||
classifier=v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a
|
||||
landing=v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a
|
||||
form=v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a
|
||||
tables=v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a
|
||||
browse=v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a
|
||||
archive=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
|
||||
transmittal=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
|
||||
classifier=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
|
||||
landing=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
|
||||
form=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
|
||||
tables=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
|
||||
browse=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
|
||||
|
|
|
|||
|
|
@ -1770,7 +1770,7 @@ body.is-elevated::after {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 13:55:36 · 0847c7a</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
|
|||
Loading…
Reference in a new issue