fix(classifier): search opens only the path to a hit, not the whole tree
Searching no longer force-expands every folder. computeVisible now returns an "open" set holding just the connector folders on the path down to each match; those open to expose the hit, while off-path branches and terminal nodes keep their real collapse state (and honest ▶/▼ arrows). Reshaping the tree is the user's call — the root's expand-all is one click away. Test: a deep file hit opens its branch and leaves the sibling collapsed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
15ce7098a0
commit
e1c479dba5
2 changed files with 50 additions and 12 deletions
|
|
@ -93,30 +93,42 @@
|
||||||
var visible = null; // { folders, files } while filtering, else null
|
var visible = null; // { folders, files } while filtering, else null
|
||||||
function computeVisible() {
|
function computeVisible() {
|
||||||
var c = window.app.modules.classify;
|
var c = window.app.modules.classify;
|
||||||
var folders = Object.create(null), files = Object.create(null);
|
var folders = Object.create(null), files = Object.create(null), open = Object.create(null);
|
||||||
var nf = filterActive();
|
var nf = filterActive();
|
||||||
function walk(folder, ancMatched) {
|
function walk(folder, ancMatched) {
|
||||||
var matched = ancMatched || (nf && nameHit(folder.path || folder.name));
|
var selfMatch = nf && nameHit(folder.path || folder.name);
|
||||||
var show = false, hasFile = false;
|
var matched = ancMatched || selfMatch;
|
||||||
|
var show = false, hasFile = false, descMatch = false;
|
||||||
(folder.children || []).forEach(function (ch) {
|
(folder.children || []).forEach(function (ch) {
|
||||||
var r = walk(ch, matched);
|
var r = walk(ch, matched);
|
||||||
if (r.show) show = true;
|
if (r.show) show = true;
|
||||||
if (r.hasFile) hasFile = true;
|
if (r.hasFile) hasFile = true;
|
||||||
|
if (r.subtreeMatch) descMatch = true; // a child leads to a match
|
||||||
});
|
});
|
||||||
(folder.files || []).forEach(function (f) {
|
(folder.files || []).forEach(function (f) {
|
||||||
hasFile = true;
|
hasFile = true;
|
||||||
if (!classifyAllows(f)) return;
|
if (!classifyAllows(f)) return;
|
||||||
if (!nf || matched || nameHit(c.srcKeyForFile(f))) { files[c.srcKeyForFile(f)] = true; show = true; }
|
var fileMatch = nf && nameHit(c.srcKeyForFile(f));
|
||||||
|
if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; }
|
||||||
|
if (fileMatch) descMatch = true; // a match sits directly in this folder
|
||||||
});
|
});
|
||||||
if (matched) show = true;
|
if (matched) show = true;
|
||||||
// "Show Empty" off → hide folders whose whole subtree holds no files.
|
// "Show Empty" off → hide folders whose whole subtree holds no files.
|
||||||
if (!hasFile && !showEmpty && !matched) show = false;
|
if (!hasFile && !showEmpty && !matched) show = false;
|
||||||
if (show) folders[folder.path] = true;
|
if (show) folders[folder.path] = true;
|
||||||
return { show: show, hasFile: hasFile };
|
// 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); });
|
(window.app.folderTree || []).forEach(function (root) { walk(root, false); });
|
||||||
return { folders: folders, files: files };
|
return { folders: folders, files: files, open: open };
|
||||||
}
|
}
|
||||||
|
// 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 folderShown(folder) { return !visible || !!visible.folders[folder.path]; }
|
||||||
function fileShown(file) {
|
function fileShown(file) {
|
||||||
if (!classifyAllows(file)) return false;
|
if (!classifyAllows(file)) return false;
|
||||||
|
|
@ -272,7 +284,7 @@
|
||||||
// expandable so its files can be revealed and dragged.
|
// expandable so its files can be revealed and dragged.
|
||||||
|| (classifyOn() && folder.files && folder.files.length > 0);
|
|| (classifyOn() && folder.files && folder.files.length > 0);
|
||||||
if (mightHaveChildren) {
|
if (mightHaveChildren) {
|
||||||
toggle.textContent = (folder.expanded || filterActive()) ? '▼' : '▶';
|
toggle.textContent = (folder.expanded || autoOpen(folder)) ? '▼' : '▶';
|
||||||
toggle.addEventListener('click', (e) => {
|
toggle.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const recursive = e.ctrlKey || e.metaKey;
|
const recursive = e.ctrlKey || e.metaKey;
|
||||||
|
|
@ -350,9 +362,9 @@
|
||||||
|
|
||||||
div.appendChild(item);
|
div.appendChild(item);
|
||||||
|
|
||||||
// Children — when expanded, or auto-expanded to reveal a NAME-search
|
// Children — when expanded, or opened on the path to a search hit below.
|
||||||
// match. The Show toggles only hide/show; they never force-expand.
|
// The Show toggles never force-expand; search opens only connector folders.
|
||||||
if ((folder.expanded || filterActive()) && folder.children && folder.children.length > 0) {
|
if ((folder.expanded || autoOpen(folder)) && folder.children && folder.children.length > 0) {
|
||||||
const childrenDiv = document.createElement('div');
|
const childrenDiv = document.createElement('div');
|
||||||
childrenDiv.className = 'folder-children';
|
childrenDiv.className = 'folder-children';
|
||||||
folder.children.forEach(child => {
|
folder.children.forEach(child => {
|
||||||
|
|
@ -364,8 +376,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classify mode: list this folder's own files (draggable leaves) when
|
// Classify mode: list this folder's own files (draggable leaves) when
|
||||||
// expanded (or auto-expanded by a name search), so they can be dropped.
|
// expanded (or opened to reveal a search hit), so they can be dropped.
|
||||||
if (classifyOn() && (folder.expanded || filterActive()) && folder.files && folder.files.length > 0) {
|
if (classifyOn() && (folder.expanded || autoOpen(folder)) && folder.files && folder.files.length > 0) {
|
||||||
const filesDiv = document.createElement('div');
|
const filesDiv = document.createElement('div');
|
||||||
filesDiv.className = 'folder-children folder-files';
|
filesDiv.className = 'folder-children folder-files';
|
||||||
folder.files.forEach(function (file) {
|
folder.files.forEach(function (file) {
|
||||||
|
|
|
||||||
|
|
@ -786,3 +786,29 @@ test('toggling a Show filter preserves collapse state (no force-expand)', async
|
||||||
expect(r.afterToggle).toEqual(['Project']); // Show toggle leaves it 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', 'Project/Sub']); // name search auto-expands to the match
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('search opens only the branch with a hit, leaving siblings collapsed', 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: [
|
||||||
|
{ originalFilename: 'Switchgear Spec', extension: 'pdf', folderPath: 'Project/Electrical' },
|
||||||
|
] },
|
||||||
|
{ name: 'Civil', path: 'Project/Civil', expanded: false, scanState: 'done', children: [], files: [
|
||||||
|
{ originalFilename: 'Site Plan', extension: 'pdf', folderPath: 'Project/Civil' },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
const tree = window.app.modules.tree;
|
||||||
|
tree.render();
|
||||||
|
tree.setNameFilter('switchgear'); // a file deep in the 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']);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue