feat(classifier): live filter box above each file tree (reveals matches + path)
Adds an autofilter input above the source tree and above each target tree. Typing substring-matches (ANDing space-separated terms) against the full file path/name (and folder/node names) and reveals every match with the folder hierarchy leading to it — non-matching branches collapse out, matching branches auto-expand. So you can type "master deliverables list" and jump straight to it. - Source tree (tree.js): one-pass visible-set over path+name; composes with the Show Unassigned/Assigned/Excluded toggles; auto-expands to reveal hits. - Target trees (target-tree.js): tracking + transmittal nodes are filter-aware (match node names + each placed file's original/derived name); one shared query mirrored across both tab inputs. Tests: source-tree path reveal + tracking-tree node filter (classify.spec.js -> 36). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9851cc4463
commit
c61cac7c8f
6 changed files with 203 additions and 51 deletions
|
|
@ -150,6 +150,15 @@
|
|||
.classify-filters { display: inline-flex; flex-wrap: wrap; gap: 0.3rem 0.75rem; justify-content: flex-end; }
|
||||
.classify-filters .filter-count { color: var(--text-muted); font-size: 0.85em; }
|
||||
|
||||
/* Live filter box above a file tree. */
|
||||
.tree-filter {
|
||||
width: 100%; box-sizing: border-box; margin: 0.25rem 0;
|
||||
padding: 0.25rem 0.5rem; font: inherit; font-size: 0.85rem;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
background: var(--bg); color: var(--text);
|
||||
}
|
||||
.tree-filter:focus { outline: none; border-color: var(--primary); }
|
||||
|
||||
.folder-stats,
|
||||
.file-stats {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -150,6 +150,9 @@
|
|||
exportDatasetBtn: document.getElementById('exportDatasetBtn'),
|
||||
importDatasetBtn: document.getElementById('importDatasetBtn'),
|
||||
importDatasetInput: document.getElementById('importDatasetInput'),
|
||||
treeFilterInput: document.getElementById('treeFilterInput'),
|
||||
trackingFilterInput: document.getElementById('trackingFilterInput'),
|
||||
transmittalFilterInput: document.getElementById('transmittalFilterInput'),
|
||||
|
||||
// Folder tree
|
||||
folderTree: document.getElementById('folderTree'),
|
||||
|
|
@ -347,6 +350,20 @@
|
|||
if (app.dom.modeClassifyBtn) app.dom.modeClassifyBtn.addEventListener('click', function () { setMode('classify'); });
|
||||
if (app.dom.copyOutputBtn) app.dom.copyOutputBtn.addEventListener('click', function () { app.modules.copy.run(); });
|
||||
|
||||
// Live source-tree filter (matches file path + name; reveals the hierarchy).
|
||||
if (app.dom.treeFilterInput) app.dom.treeFilterInput.addEventListener('input', function () {
|
||||
if (app.modules.tree && app.modules.tree.setNameFilter) app.modules.tree.setNameFilter(this.value);
|
||||
});
|
||||
// Target-tree filter — both tabs share one query (mirrored across inputs).
|
||||
function targetFilter(val) {
|
||||
if (app.dom.trackingFilterInput) app.dom.trackingFilterInput.value = val;
|
||||
if (app.dom.transmittalFilterInput) app.dom.transmittalFilterInput.value = val;
|
||||
if (app.modules.targetTree && app.modules.targetTree.setNameFilter) app.modules.targetTree.setNameFilter(val);
|
||||
}
|
||||
[app.dom.trackingFilterInput, app.dom.transmittalFilterInput].forEach(function (inp) {
|
||||
if (inp) inp.addEventListener('input', function () { targetFilter(this.value); });
|
||||
});
|
||||
|
||||
// Dataset export / import (round-trip the classification through a JSON file).
|
||||
if (app.dom.exportDatasetBtn) app.dom.exportDatasetBtn.addEventListener('click', exportDataset);
|
||||
if (app.dom.importDatasetBtn) app.dom.importDatasetBtn.addEventListener('click', function () { app.dom.importDatasetInput.click(); });
|
||||
|
|
|
|||
|
|
@ -180,40 +180,67 @@
|
|||
return box;
|
||||
}
|
||||
|
||||
// Tracking tree (recursive)
|
||||
// ── name filter (the autofilter box above the target trees) ────────────
|
||||
var rfTerms = [];
|
||||
function setNameFilter(q) {
|
||||
rfTerms = String(q || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||
render();
|
||||
}
|
||||
function rfActive() { return rfTerms.length > 0; }
|
||||
function rfHit(text) {
|
||||
if (!rfTerms.length) return true;
|
||||
var t = String(text || '').toLowerCase();
|
||||
for (var i = 0; i < rfTerms.length; i++) { if (t.indexOf(rfTerms[i]) === -1) return false; }
|
||||
return true;
|
||||
}
|
||||
// A placed-file row matches on its original name or its derived ZDDC name.
|
||||
function fileRowMatches(f) {
|
||||
var orig = f.originalFilename + (f.extension ? '.' + f.extension : '');
|
||||
return rfHit(orig) || rfHit(C().deriveTarget(f).filename || '');
|
||||
}
|
||||
|
||||
// Tracking tree (recursive, filter-aware — a match reveals its whole path).
|
||||
function renderTrackingInto(container, nodes, placedMap) {
|
||||
container.textContent = '';
|
||||
if (!nodes.length) {
|
||||
container.appendChild(el('div', 'target-empty', 'No tracking folders yet — “+ Root folder” to start.'));
|
||||
return;
|
||||
}
|
||||
nodes.forEach(function (n) { container.appendChild(trackingNode(n, placedMap)); });
|
||||
nodes.forEach(function (n) { var e = trackingNode(n, placedMap, false); if (e) container.appendChild(e); });
|
||||
if (rfActive() && !container.children.length) {
|
||||
container.appendChild(el('div', 'target-empty', 'No matches in the tracking tree.'));
|
||||
}
|
||||
function trackingNode(n, placedMap) {
|
||||
}
|
||||
function trackingNode(n, placedMap, ancMatched) {
|
||||
var matched = ancMatched || rfHit(n.name);
|
||||
var isLeaf = (n.children || []).length === 0;
|
||||
var expanded = !collapsed[n.id] || rfActive(); // auto-expand to reveal matches
|
||||
var childEls = [];
|
||||
if (expanded || rfActive()) {
|
||||
(n.children || []).forEach(function (c) { var ce = trackingNode(c, placedMap, matched); if (ce) childEls.push(ce); });
|
||||
}
|
||||
var placed = placedMap[n.id] || [];
|
||||
var shownFiles = (rfActive() && !matched) ? placed.filter(fileRowMatches) : placed;
|
||||
if (rfActive() && !matched && !childEls.length && !shownFiles.length) return null;
|
||||
|
||||
var wrap = el('div', 'tnode' + (isLeaf ? ' tnode--leaf' : ''));
|
||||
wrap.dataset.id = n.id;
|
||||
var row = el('div', 'tnode__row');
|
||||
|
||||
var toggle = el('button', 'tnode__toggle', isLeaf ? '·' : (collapsed[n.id] ? '▸' : '▾'));
|
||||
var toggle = el('button', 'tnode__toggle', isLeaf ? '·' : (expanded ? '▾' : '▸'));
|
||||
if (!isLeaf) toggle.dataset.act = 'toggle';
|
||||
row.appendChild(toggle);
|
||||
row.appendChild(el('span', 'tnode__name', n.name));
|
||||
|
||||
var placed = placedMap[n.id] || [];
|
||||
if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length)));
|
||||
|
||||
row.appendChild(nodeActions([
|
||||
{ act: 'add', label: '+', title: 'Add child folder' },
|
||||
{ act: 'rename', label: '✎', title: 'Rename' },
|
||||
{ act: 'del', label: '🗑', title: 'Delete' },
|
||||
]));
|
||||
wrap.appendChild(row);
|
||||
|
||||
if (placed.length) wrap.appendChild(fileList(placed));
|
||||
if (!isLeaf && !collapsed[n.id]) {
|
||||
if (shownFiles.length) wrap.appendChild(fileList(shownFiles));
|
||||
if (!isLeaf && expanded && childEls.length) {
|
||||
var kids = el('div', 'tnode__children');
|
||||
(n.children || []).forEach(function (c) { kids.appendChild(trackingNode(c, placedMap)); });
|
||||
childEls.forEach(function (ce) { kids.appendChild(ce); });
|
||||
wrap.appendChild(kids);
|
||||
}
|
||||
return wrap;
|
||||
|
|
@ -226,20 +253,14 @@
|
|||
container.appendChild(el('div', 'target-empty', 'No parties yet — “+ Party” to start.'));
|
||||
return;
|
||||
}
|
||||
parties.forEach(function (p) { container.appendChild(partyNode(p, placedMap)); });
|
||||
parties.forEach(function (p) { var e = partyNode(p, placedMap); if (e) container.appendChild(e); });
|
||||
if (rfActive() && !container.children.length) {
|
||||
container.appendChild(el('div', 'target-empty', 'No matches in the transmittal tree.'));
|
||||
}
|
||||
}
|
||||
function partyNode(party, placedMap) {
|
||||
var wrap = el('div', 'tnode tnode--party');
|
||||
wrap.dataset.id = party.id;
|
||||
var row = el('div', 'tnode__row');
|
||||
row.appendChild(el('span', 'tnode__icon', '🏢'));
|
||||
row.appendChild(el('span', 'tnode__name', party.name));
|
||||
row.appendChild(nodeActions([
|
||||
{ act: 'rename-party', label: '✎', title: 'Rename party' },
|
||||
{ act: 'del-party', label: '🗑', title: 'Delete party' },
|
||||
]));
|
||||
wrap.appendChild(row);
|
||||
|
||||
var partyMatch = rfHit(party.name);
|
||||
var slotEls = [], anyBin = false;
|
||||
SLOTS.forEach(function (slot) {
|
||||
var slotNode = (party.children || []).filter(function (s) { return s.slot === slot; })[0];
|
||||
var sw = el('div', 'tslot');
|
||||
|
|
@ -256,22 +277,39 @@
|
|||
sw.appendChild(binForm(party.id, slot));
|
||||
}
|
||||
(slotNode ? slotNode.children : []).forEach(function (bin) {
|
||||
sw.appendChild(binNode(bin, placedMap));
|
||||
var be = binNode(bin, placedMap, partyMatch);
|
||||
if (be) { sw.appendChild(be); anyBin = true; }
|
||||
});
|
||||
wrap.appendChild(sw);
|
||||
slotEls.push(sw);
|
||||
});
|
||||
if (rfActive() && !partyMatch && !anyBin) return null;
|
||||
|
||||
var wrap = el('div', 'tnode tnode--party');
|
||||
wrap.dataset.id = party.id;
|
||||
var row = el('div', 'tnode__row');
|
||||
row.appendChild(el('span', 'tnode__icon', '🏢'));
|
||||
row.appendChild(el('span', 'tnode__name', party.name));
|
||||
row.appendChild(nodeActions([
|
||||
{ act: 'rename-party', label: '✎', title: 'Rename party' },
|
||||
{ act: 'del-party', label: '🗑', title: 'Delete party' },
|
||||
]));
|
||||
wrap.appendChild(row);
|
||||
slotEls.forEach(function (sw) { wrap.appendChild(sw); });
|
||||
return wrap;
|
||||
}
|
||||
function binNode(bin, placedMap) {
|
||||
function binNode(bin, placedMap, ancMatched) {
|
||||
var matched = ancMatched || rfHit(bin.name || '');
|
||||
var placed = placedMap[bin.id] || [];
|
||||
var shownFiles = (rfActive() && !matched) ? placed.filter(fileRowMatches) : placed;
|
||||
if (rfActive() && !matched && !shownFiles.length) return null;
|
||||
var wrap = el('div', 'tnode tnode--bin');
|
||||
wrap.dataset.id = bin.id;
|
||||
var row = el('div', 'tnode__row');
|
||||
row.appendChild(el('span', 'tnode__name', bin.name || '(invalid — set date/seq)'));
|
||||
var placed = placedMap[bin.id] || [];
|
||||
if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length)));
|
||||
row.appendChild(nodeActions([{ act: 'del', label: '🗑', title: 'Delete transmittal' }]));
|
||||
wrap.appendChild(row);
|
||||
if (placed.length) wrap.appendChild(fileList(placed));
|
||||
if (shownFiles.length) wrap.appendChild(fileList(shownFiles));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
|
|
@ -490,6 +528,7 @@
|
|||
render: render,
|
||||
showTab: showTab,
|
||||
activeAxis: activeAxis,
|
||||
setNameFilter: setNameFilter,
|
||||
reveal: reveal,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -63,18 +63,55 @@
|
|||
var assigned = a && (activeAxis() === 'transmittal' ? a.transmittalNodeId : a.trackingNodeId);
|
||||
return assigned ? 'assigned' : 'unassigned';
|
||||
}
|
||||
function fileVisible(file) { return !!showFilters[fileCategory(file)]; }
|
||||
function subtreeVisibleCount(folder) {
|
||||
var n = 0;
|
||||
subtreeFiles(folder).forEach(function (f) { if (fileVisible(f)) n++; });
|
||||
return n;
|
||||
function classifyAllows(file) { return !classifyOn() || !!showFilters[fileCategory(file)]; }
|
||||
|
||||
// ── name filter (the autofilter box above the tree) ────────────────────
|
||||
// Live substring search over each file's full path+name (and folder names),
|
||||
// ANDing space-separated terms. Matches reveal their whole folder hierarchy.
|
||||
var nameFilter = '', filterTerms = [];
|
||||
function setNameFilter(q) {
|
||||
nameFilter = (q || '').trim();
|
||||
filterTerms = nameFilter.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
render();
|
||||
}
|
||||
// Hide a folder only when it's fully scanned (so we never hide one that may
|
||||
// still reveal files) and the active filters leave nothing visible in it.
|
||||
function folderHidden(folder) {
|
||||
if (!classifyOn() || allFiltersOn()) return false;
|
||||
if (folder.scanState && folder.scanState !== 'done') return false;
|
||||
return subtreeVisibleCount(folder) === 0;
|
||||
function filterActive() { return filterTerms.length > 0; }
|
||||
function nameHit(text) {
|
||||
if (!filterTerms.length) return true;
|
||||
var t = String(text || '').toLowerCase();
|
||||
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()); }
|
||||
// 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
|
||||
// ancestor) matches, or anything inside it shows — so the path to a hit is
|
||||
// always revealed.
|
||||
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);
|
||||
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; });
|
||||
(folder.files || []).forEach(function (f) {
|
||||
if (!classifyAllows(f)) return;
|
||||
if (!nf || matched || nameHit(c.srcKeyForFile(f))) { files[c.srcKeyForFile(f)] = true; show = true; }
|
||||
});
|
||||
if (matched) show = true;
|
||||
if (show) folders[folder.path] = true;
|
||||
return show;
|
||||
}
|
||||
(window.app.folderTree || []).forEach(function (root) { walk(root, false); });
|
||||
return { folders: folders, files: files };
|
||||
}
|
||||
function folderShown(folder) { return !visible || !!visible.folders[folder.path]; }
|
||||
function fileShown(file) {
|
||||
if (!classifyAllows(file)) return false;
|
||||
return !visible || !!visible.files[window.app.modules.classify.srcKeyForFile(file)];
|
||||
}
|
||||
// All scanned files (for the per-bucket counts on the filter checkboxes).
|
||||
function allClassifyFiles() {
|
||||
|
|
@ -100,6 +137,7 @@
|
|||
wireClassifyInteractions();
|
||||
container.innerHTML = '';
|
||||
updateFilterCounts();
|
||||
visible = anyFilter() ? computeVisible() : null;
|
||||
|
||||
if (window.app.folderTree.length === 0) {
|
||||
container.innerHTML = '<div class="tree-empty">No folders found</div>';
|
||||
|
|
@ -107,12 +145,13 @@
|
|||
}
|
||||
|
||||
window.app.folderTree.forEach(folder => {
|
||||
if (folderHidden(folder)) return;
|
||||
if (!folderShown(folder)) return;
|
||||
const element = createFolderElement(folder);
|
||||
container.appendChild(element);
|
||||
});
|
||||
if (classifyOn() && !container.children.length) {
|
||||
container.innerHTML = '<div class="tree-empty">Nothing matches the current filters.</div>';
|
||||
if (!container.children.length) {
|
||||
container.innerHTML = '<div class="tree-empty">'
|
||||
+ (filterActive() ? 'No files match “' + nameFilter + '”.' : 'Nothing matches the current filters.') + '</div>';
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
|
|
@ -220,7 +259,7 @@
|
|||
// expandable so its files can be revealed and dragged.
|
||||
|| (classifyOn() && folder.files && folder.files.length > 0);
|
||||
if (mightHaveChildren) {
|
||||
toggle.textContent = folder.expanded ? '▼' : '▶';
|
||||
toggle.textContent = (folder.expanded || visible) ? '▼' : '▶';
|
||||
toggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const recursive = e.ctrlKey || e.metaKey;
|
||||
|
|
@ -298,12 +337,12 @@
|
|||
|
||||
div.appendChild(item);
|
||||
|
||||
// Children (if expanded)
|
||||
if (folder.expanded && folder.children && folder.children.length > 0) {
|
||||
// Children — when expanded, or auto-expanded to reveal a filter match.
|
||||
if ((folder.expanded || visible) && folder.children && folder.children.length > 0) {
|
||||
const childrenDiv = document.createElement('div');
|
||||
childrenDiv.className = 'folder-children';
|
||||
folder.children.forEach(child => {
|
||||
if (folderHidden(child)) return;
|
||||
if (!folderShown(child)) return;
|
||||
const childElement = createFolderElement(child, level + 1);
|
||||
childrenDiv.appendChild(childElement);
|
||||
});
|
||||
|
|
@ -311,12 +350,12 @@
|
|||
}
|
||||
|
||||
// Classify mode: list this folder's own files (draggable leaves) when
|
||||
// expanded, so they can be dropped onto the target trees.
|
||||
if (classifyOn() && folder.expanded && folder.files && folder.files.length > 0) {
|
||||
// expanded (or auto-expanded by a filter), so they can be dropped.
|
||||
if (classifyOn() && (folder.expanded || visible) && folder.files && folder.files.length > 0) {
|
||||
const filesDiv = document.createElement('div');
|
||||
filesDiv.className = 'folder-children folder-files';
|
||||
folder.files.forEach(function (file) {
|
||||
if (classifyOn() && !fileVisible(file)) return;
|
||||
if (!fileShown(file)) return;
|
||||
filesDiv.appendChild(createFileElement(file, level + 1));
|
||||
});
|
||||
div.appendChild(filesDiv);
|
||||
|
|
@ -894,6 +933,7 @@
|
|||
expandAll,
|
||||
selectAll,
|
||||
revealFile,
|
||||
setShowFilters
|
||||
setShowFilters,
|
||||
setNameFilter
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@
|
|||
<span id="selectedFoldersCount" class="folder-count">0 folders selected</span>
|
||||
</div>
|
||||
</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">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
|
|
@ -168,6 +170,8 @@
|
|||
<button id="addTrackingRootBtn" class="btn btn-sm btn-secondary">+ Root folder</button>
|
||||
<span class="target-hint">Folders join with “-” into the tracking number; the leaf folder is the revision — name it like “A (IFR)”.</span>
|
||||
</div>
|
||||
<input type="search" id="trackingFilterInput" class="tree-filter target-filter" spellcheck="false"
|
||||
placeholder="Filter the tracking tree…" aria-label="Filter tracking tree">
|
||||
<div id="trackingTree" class="target-tree"></div>
|
||||
</section>
|
||||
<section id="transmittalPanel" class="target-panel" hidden>
|
||||
|
|
@ -175,6 +179,8 @@
|
|||
<button id="addPartyBtn" class="btn btn-sm btn-secondary">+ Party</button>
|
||||
<span class="target-hint"><party>/{received,issued}/<transmittal>. Drag files (or a whole folder) into a transmittal.</span>
|
||||
</div>
|
||||
<input type="search" id="transmittalFilterInput" class="tree-filter target-filter" spellcheck="false"
|
||||
placeholder="Filter the transmittal tree…" aria-label="Filter transmittal tree">
|
||||
<div id="transmittalTree" class="target-tree"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -701,3 +701,44 @@ test('dataset (filename-based): import reconstruction rebuilds tracking + shared
|
|||
expect(r.bins).toBe(1); // shared transmittal → single bin (dedup)
|
||||
expect(r.excluded).toBe(true);
|
||||
});
|
||||
|
||||
test('source-tree filter reveals matches with their folder hierarchy', 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: 'Master Deliverables List', extension: 'xlsx', folderPath: 'Project/Electrical' },
|
||||
{ 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' },
|
||||
] },
|
||||
],
|
||||
}];
|
||||
window.app.modules.tree.render();
|
||||
window.app.modules.tree.setNameFilter('master deliverables');
|
||||
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),
|
||||
};
|
||||
});
|
||||
expect(r.files).toEqual(['Master Deliverables List.xlsx']); // only the match shown
|
||||
expect(r.folders).toEqual(['Project', 'Project/Electrical']); // path revealed; Civil hidden
|
||||
});
|
||||
|
||||
test('tracking-tree filter reveals matching nodes and hides the rest', async ({ page }) => {
|
||||
await page.click('#modeClassifyBtn');
|
||||
const names = await page.evaluate(() => {
|
||||
const c = window.app.modules.classify;
|
||||
c.reset();
|
||||
c.addTrackingPath(null, c.parseFolderLevels('CPO-0001_0 (IFU)'));
|
||||
c.addTrackingPath(null, c.parseFolderLevels('XYZ-0009_A (IFR)'));
|
||||
window.app.modules.targetTree.render();
|
||||
window.app.modules.targetTree.setNameFilter('CPO');
|
||||
return Array.from(document.querySelectorAll('#trackingTree .tnode__name')).map((e) => e.textContent);
|
||||
});
|
||||
expect(names).toContain('CPO');
|
||||
expect(names).toContain('0001');
|
||||
expect(names).not.toContain('XYZ');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue