feat(classifier): Hide Assigned filter, left-aligned node controls, brace-expand add
Classify & Copy polish — in either target tab the goal is to assign or exclude
every left-pane file until nothing remains:
- Hide Assigned checkbox (classify mode, in the folder-tree pane header):
collapses the source tree to only what's left on the ACTIVE axis — hides
files already assigned in the current tab (or excluded) and any folder whose
scanned subtree is thereby empty. Re-renders on tab switch; target-tree
exposes activeAxis().
- Node add/edit/delete controls moved to the LEFT of the level name and made
always-visible (was right-aligned + hover-only), so building/pruning the
tracking and transmittal trees is one click.
- Brace expansion in the add-folder box: "BMB-187023-{PM,EL,EM}-MOM-
{0001-0002,0005}_A (IFR)" creates all 9 folders — {a,b} alternation +
{N-M} zero-padded numeric ranges, cartesian product across groups; a
multi-create is confirmed first. New classify.expandFolderPattern().
Tests: expandFolderPattern unit cases + a Hide-Assigned DOM test
(classify.spec.js → 29 passed; classifier.spec.js → 4 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6d132572d3
commit
8f839fc0c9
7 changed files with 203 additions and 17 deletions
|
|
@ -405,8 +405,9 @@
|
||||||
background: var(--primary); color: var(--bg);
|
background: var(--primary); color: var(--bg);
|
||||||
border-radius: 999px; padding: 0 0.4rem; font-size: 0.7rem; font-weight: 600;
|
border-radius: 999px; padding: 0 0.4rem; font-size: 0.7rem; font-weight: 600;
|
||||||
}
|
}
|
||||||
.tnode__actions { margin-left: auto; display: inline-flex; gap: 0.1rem; opacity: 0; transition: opacity 0.12s; }
|
/* Node CRUD controls sit to the LEFT of the level name (always visible) so
|
||||||
.tnode__row:hover .tnode__actions, .tslot__row:hover .tnode__actions { opacity: 1; }
|
building/pruning the tree is a one-click affordance, not a hover-hunt. */
|
||||||
|
.tnode__actions { display: inline-flex; gap: 0.1rem; margin-right: 0.15rem; flex: 0 0 auto; }
|
||||||
.tnode__act {
|
.tnode__act {
|
||||||
border: 1px solid var(--border); background: var(--bg);
|
border: 1px solid var(--border); background: var(--bg);
|
||||||
border-radius: var(--radius); cursor: pointer;
|
border-radius: var(--radius); cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,10 @@
|
||||||
exportHashesBtn: document.getElementById('exportHashesBtn'),
|
exportHashesBtn: document.getElementById('exportHashesBtn'),
|
||||||
sha256Checkbox: document.getElementById('sha256Checkbox'),
|
sha256Checkbox: document.getElementById('sha256Checkbox'),
|
||||||
hideCompliantCheckbox: document.getElementById('hideCompliantCheckbox'),
|
hideCompliantCheckbox: document.getElementById('hideCompliantCheckbox'),
|
||||||
|
hideCompliantLabel: document.getElementById('hideCompliantLabel'),
|
||||||
|
hideAssignedCheckbox: document.getElementById('hideAssignedCheckbox'),
|
||||||
|
hideAssignedLabel: document.getElementById('hideAssignedLabel'),
|
||||||
|
|
||||||
// Folder tree
|
// Folder tree
|
||||||
folderTree: document.getElementById('folderTree'),
|
folderTree: document.getElementById('folderTree'),
|
||||||
folderTreePane: document.getElementById('folderTreePane'),
|
folderTreePane: document.getElementById('folderTreePane'),
|
||||||
|
|
@ -183,6 +186,10 @@
|
||||||
app.dom.modeClassifyBtn.classList.toggle('active', classify);
|
app.dom.modeClassifyBtn.classList.toggle('active', classify);
|
||||||
if (app.dom.spreadsheetPane) app.dom.spreadsheetPane.hidden = classify;
|
if (app.dom.spreadsheetPane) app.dom.spreadsheetPane.hidden = classify;
|
||||||
if (app.dom.targetPane) app.dom.targetPane.hidden = !classify;
|
if (app.dom.targetPane) app.dom.targetPane.hidden = !classify;
|
||||||
|
// Mode-specific source-tree filters: "Hide Compliant" is for the rename
|
||||||
|
// grid; "Hide Assigned" is for the classify workflow.
|
||||||
|
if (app.dom.hideCompliantLabel) app.dom.hideCompliantLabel.hidden = classify;
|
||||||
|
if (app.dom.hideAssignedLabel) app.dom.hideAssignedLabel.hidden = !classify;
|
||||||
app.modules.classify.setEnabled(classify);
|
app.modules.classify.setEnabled(classify);
|
||||||
if (classify && app.modules.targetTree) {
|
if (classify && app.modules.targetTree) {
|
||||||
app.modules.targetTree.init();
|
app.modules.targetTree.init();
|
||||||
|
|
@ -215,7 +222,16 @@
|
||||||
|
|
||||||
// Hide compliant toggle
|
// Hide compliant toggle
|
||||||
app.dom.hideCompliantCheckbox.addEventListener('change', handleHideCompliantToggle);
|
app.dom.hideCompliantCheckbox.addEventListener('change', handleHideCompliantToggle);
|
||||||
|
|
||||||
|
// Hide assigned toggle (classify mode — filters the source tree)
|
||||||
|
if (app.dom.hideAssignedCheckbox) {
|
||||||
|
app.dom.hideAssignedCheckbox.addEventListener('change', function () {
|
||||||
|
if (app.modules.tree && app.modules.tree.setHideAssigned) {
|
||||||
|
app.modules.tree.setHideAssigned(this.checked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Collapse tree button
|
// Collapse tree button
|
||||||
app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree);
|
app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,61 @@
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── add-folder pattern expansion ─────────────────────────────────────────
|
||||||
|
// Brace expansion for the add-folder box. Supports (non-nested) groups:
|
||||||
|
// {a,b,c} → alternation: a | b | c
|
||||||
|
// {0001-0002} → numeric range, zero-padded to the operands' width
|
||||||
|
// {0001-0002,0005} → mix ranges and literals in one group
|
||||||
|
// Multiple groups expand as a cartesian product, e.g.
|
||||||
|
// "X-{PM,EL}-{0001-0002,0005}_A (IFR)" → 6 names.
|
||||||
|
// A pattern with no braces returns itself (one name). Unbalanced braces are
|
||||||
|
// treated literally so the user never silently loses input.
|
||||||
|
function expandGroup(body) {
|
||||||
|
var out = [];
|
||||||
|
String(body).split(',').forEach(function (piece) {
|
||||||
|
var m = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(piece);
|
||||||
|
if (m) {
|
||||||
|
var a = m[1], b = m[2];
|
||||||
|
var start = parseInt(a, 10), end = parseInt(b, 10);
|
||||||
|
// Pad when either operand carries a leading zero (e.g. 0001).
|
||||||
|
var width = (a.length > 1 && a[0] === '0') || (b.length > 1 && b[0] === '0')
|
||||||
|
? Math.max(a.length, b.length) : 0;
|
||||||
|
var step = start <= end ? 1 : -1;
|
||||||
|
for (var v = start; step > 0 ? v <= end : v >= end; v += step) {
|
||||||
|
out.push(width ? String(v).padStart(width, '0') : String(v));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push(piece);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function expandFolderPattern(pattern) {
|
||||||
|
var s = String(pattern == null ? '' : pattern);
|
||||||
|
var parts = []; // each: {lit} or {opts:[...]}
|
||||||
|
var i = 0;
|
||||||
|
while (i < s.length) {
|
||||||
|
var open = s.indexOf('{', i);
|
||||||
|
if (open === -1) { parts.push({ lit: s.slice(i) }); break; }
|
||||||
|
var close = s.indexOf('}', open);
|
||||||
|
if (close === -1) { parts.push({ lit: s.slice(i) }); break; } // unbalanced → literal
|
||||||
|
if (open > i) parts.push({ lit: s.slice(i, open) });
|
||||||
|
parts.push({ opts: expandGroup(s.slice(open + 1, close)) });
|
||||||
|
i = close + 1;
|
||||||
|
}
|
||||||
|
var results = [''];
|
||||||
|
parts.forEach(function (p) {
|
||||||
|
var opts = p.lit != null ? [p.lit] : p.opts;
|
||||||
|
var next = [];
|
||||||
|
results.forEach(function (prefix) {
|
||||||
|
opts.forEach(function (o) { next.push(prefix + o); });
|
||||||
|
});
|
||||||
|
results = next;
|
||||||
|
});
|
||||||
|
// Trim + drop empties so a stray comma can't create a blank folder.
|
||||||
|
return results.map(function (r) { return r.trim(); }).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
// ── mode ─────────────────────────────────────────────────────────────────
|
// ── mode ─────────────────────────────────────────────────────────────────
|
||||||
function setEnabled(on) { state.enabled = !!on; notify(); }
|
function setEnabled(on) { state.enabled = !!on; notify(); }
|
||||||
function isEnabled() { return state.enabled; }
|
function isEnabled() { return state.enabled; }
|
||||||
|
|
@ -412,6 +467,7 @@
|
||||||
// trees
|
// trees
|
||||||
addTrackingNode: addTrackingNode, addParty: addParty,
|
addTrackingNode: addTrackingNode, addParty: addParty,
|
||||||
addTransmittalBin: addTransmittalBin, renameNode: renameNode, deleteNode: deleteNode,
|
addTransmittalBin: addTransmittalBin, renameNode: renameNode, deleteNode: deleteNode,
|
||||||
|
expandFolderPattern: expandFolderPattern,
|
||||||
getNode: getNode, getTrackingTree: function () { return state.trackingTree; },
|
getNode: getNode, getTrackingTree: function () { return state.trackingTree; },
|
||||||
getTransmittalTree: function () { return state.transmittalTree; },
|
getTransmittalTree: function () { return state.transmittalTree; },
|
||||||
// derive + reverse
|
// derive + reverse
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
var collapsed = {}; // nodeId -> true when collapsed (default expanded)
|
var collapsed = {}; // nodeId -> true when collapsed (default expanded)
|
||||||
var openForm = null; // { partyId, slot } when a bin form is open
|
var openForm = null; // { partyId, slot } when a bin form is open
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
|
var currentTab = 'tracking'; // 'tracking' | 'transmittal' — the active axis
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
|
|
@ -38,8 +39,9 @@
|
||||||
els.trackingTab.addEventListener('click', function () { showTab('tracking'); });
|
els.trackingTab.addEventListener('click', function () { showTab('tracking'); });
|
||||||
els.transmittalTab.addEventListener('click', function () { showTab('transmittal'); });
|
els.transmittalTab.addEventListener('click', function () { showTab('transmittal'); });
|
||||||
els.addTrackingRootBtn.addEventListener('click', function () {
|
els.addTrackingRootBtn.addEventListener('click', function () {
|
||||||
var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ"):', '');
|
var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ").\n'
|
||||||
if (name && name.trim()) C().addTrackingNode(null, name.trim());
|
+ 'Brace patterns expand: BMB-{PM,EL}-{0001-0002,0005}_A (IFR)', '');
|
||||||
|
addFoldersFromPattern(null, name);
|
||||||
});
|
});
|
||||||
els.addPartyBtn.addEventListener('click', function () {
|
els.addPartyBtn.addEventListener('click', function () {
|
||||||
var name = prompt('Party name (also the transmittal-number prefix):', '');
|
var name = prompt('Party name (also the transmittal-number prefix):', '');
|
||||||
|
|
@ -86,10 +88,29 @@
|
||||||
|
|
||||||
function showTab(which) {
|
function showTab(which) {
|
||||||
var t = which === 'transmittal';
|
var t = which === 'transmittal';
|
||||||
|
currentTab = t ? 'transmittal' : 'tracking';
|
||||||
els.trackingTab.classList.toggle('active', !t);
|
els.trackingTab.classList.toggle('active', !t);
|
||||||
els.transmittalTab.classList.toggle('active', t);
|
els.transmittalTab.classList.toggle('active', t);
|
||||||
els.trackingPanel.hidden = t;
|
els.trackingPanel.hidden = t;
|
||||||
els.transmittalPanel.hidden = !t;
|
els.transmittalPanel.hidden = !t;
|
||||||
|
// The "Hide Assigned" filter on the source tree is per-axis, so the
|
||||||
|
// visible set changes with the active tab — re-render the left tree.
|
||||||
|
if (window.app.modules.tree && window.app.modules.tree.render) window.app.modules.tree.render();
|
||||||
|
}
|
||||||
|
function activeAxis() { return currentTab === 'transmittal' ? 'transmittal' : 'tracking'; }
|
||||||
|
|
||||||
|
// Expand a brace pattern into folder names and create them (confirming a
|
||||||
|
// multi-create first). parentId null = root folders. See expandFolderPattern.
|
||||||
|
function addFoldersFromPattern(parentId, raw) {
|
||||||
|
if (!raw || !raw.trim()) return;
|
||||||
|
var names = C().expandFolderPattern(raw);
|
||||||
|
if (!names.length) return;
|
||||||
|
if (names.length > 1) {
|
||||||
|
var shown = names.slice(0, 8).join('\n');
|
||||||
|
if (names.length > 8) shown += '\n…and ' + (names.length - 8) + ' more';
|
||||||
|
if (!confirm('Create ' + names.length + ' folders?\n\n' + shown)) return;
|
||||||
|
}
|
||||||
|
names.forEach(function (nm) { C().addTrackingNode(parentId, nm); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── render ───────────────────────────────────────────────────────────────
|
// ── render ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -166,16 +187,15 @@
|
||||||
var toggle = el('button', 'tnode__toggle', isLeaf ? '·' : (collapsed[n.id] ? '▸' : '▾'));
|
var toggle = el('button', 'tnode__toggle', isLeaf ? '·' : (collapsed[n.id] ? '▸' : '▾'));
|
||||||
if (!isLeaf) toggle.dataset.act = 'toggle';
|
if (!isLeaf) toggle.dataset.act = 'toggle';
|
||||||
row.appendChild(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([
|
row.appendChild(nodeActions([
|
||||||
{ act: 'add', label: '+', title: 'Add child folder' },
|
{ act: 'add', label: '+', title: 'Add child folder' },
|
||||||
{ act: 'rename', label: '✎', title: 'Rename' },
|
{ act: 'rename', label: '✎', title: 'Rename' },
|
||||||
{ act: 'del', label: '🗑', title: 'Delete' },
|
{ act: 'del', label: '🗑', title: 'Delete' },
|
||||||
]));
|
]));
|
||||||
|
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)));
|
||||||
wrap.appendChild(row);
|
wrap.appendChild(row);
|
||||||
|
|
||||||
if (placed.length) wrap.appendChild(fileList(placed));
|
if (placed.length) wrap.appendChild(fileList(placed));
|
||||||
|
|
@ -201,11 +221,11 @@
|
||||||
wrap.dataset.id = party.id;
|
wrap.dataset.id = party.id;
|
||||||
var row = el('div', 'tnode__row');
|
var row = el('div', 'tnode__row');
|
||||||
row.appendChild(el('span', 'tnode__icon', '🏢'));
|
row.appendChild(el('span', 'tnode__icon', '🏢'));
|
||||||
row.appendChild(el('span', 'tnode__name', party.name));
|
|
||||||
row.appendChild(nodeActions([
|
row.appendChild(nodeActions([
|
||||||
{ act: 'rename-party', label: '✎', title: 'Rename party' },
|
{ act: 'rename-party', label: '✎', title: 'Rename party' },
|
||||||
{ act: 'del-party', label: '🗑', title: 'Delete party' },
|
{ act: 'del-party', label: '🗑', title: 'Delete party' },
|
||||||
]));
|
]));
|
||||||
|
row.appendChild(el('span', 'tnode__name', party.name));
|
||||||
wrap.appendChild(row);
|
wrap.appendChild(row);
|
||||||
|
|
||||||
SLOTS.forEach(function (slot) {
|
SLOTS.forEach(function (slot) {
|
||||||
|
|
@ -234,10 +254,10 @@
|
||||||
var wrap = el('div', 'tnode tnode--bin');
|
var wrap = el('div', 'tnode tnode--bin');
|
||||||
wrap.dataset.id = bin.id;
|
wrap.dataset.id = bin.id;
|
||||||
var row = el('div', 'tnode__row');
|
var row = el('div', 'tnode__row');
|
||||||
|
row.appendChild(nodeActions([{ act: 'del', label: '🗑', title: 'Delete transmittal' }]));
|
||||||
row.appendChild(el('span', 'tnode__name', bin.name || '(invalid — set date/seq)'));
|
row.appendChild(el('span', 'tnode__name', bin.name || '(invalid — set date/seq)'));
|
||||||
var placed = placedMap[bin.id] || [];
|
var placed = placedMap[bin.id] || [];
|
||||||
if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length)));
|
if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length)));
|
||||||
row.appendChild(nodeActions([{ act: 'del', label: '🗑', title: 'Delete transmittal' }]));
|
|
||||||
wrap.appendChild(row);
|
wrap.appendChild(row);
|
||||||
if (placed.length) wrap.appendChild(fileList(placed));
|
if (placed.length) wrap.appendChild(fileList(placed));
|
||||||
return wrap;
|
return wrap;
|
||||||
|
|
@ -283,8 +303,9 @@
|
||||||
var id = closestNodeId(btn);
|
var id = closestNodeId(btn);
|
||||||
if (act === 'toggle') { collapsed[id] = !collapsed[id]; render(); return; }
|
if (act === 'toggle') { collapsed[id] = !collapsed[id]; render(); return; }
|
||||||
if (act === 'add') {
|
if (act === 'add') {
|
||||||
var name = prompt('Child folder name (next tracking segment, or a leaf revision like "A (IFR)"):', '');
|
var name = prompt('Child folder name (next tracking segment, or a leaf revision like "A (IFR)").\n'
|
||||||
if (name && name.trim()) C().addTrackingNode(id, name.trim());
|
+ 'Brace patterns expand: {PM,EL,EM}-MOM-{0001-0002,0005}_A (IFR)', '');
|
||||||
|
addFoldersFromPattern(id, name);
|
||||||
} else if (act === 'rename') {
|
} else if (act === 'rename') {
|
||||||
var node = C().getNode(id);
|
var node = C().getNode(id);
|
||||||
var nn = prompt('Rename folder:', node ? node.name : '');
|
var nn = prompt('Rename folder:', node ? node.name : '');
|
||||||
|
|
@ -396,6 +417,7 @@
|
||||||
init: init,
|
init: init,
|
||||||
render: render,
|
render: render,
|
||||||
showTab: showTab,
|
showTab: showTab,
|
||||||
|
activeAxis: activeAxis,
|
||||||
reveal: reveal,
|
reveal: reveal,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,37 @@
|
||||||
return dot;
|
return dot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── "Hide Assigned" filter (classify mode) ─────────────────────────────
|
||||||
|
// The goal in either target tab is to assign-or-exclude every file, so this
|
||||||
|
// collapses the left tree down to only what's left to deal with on the
|
||||||
|
// ACTIVE axis: hide files already assigned in the current tab (or excluded),
|
||||||
|
// and any folder whose whole (scanned) subtree is thereby empty.
|
||||||
|
var hideAssigned = false;
|
||||||
|
function setHideAssigned(on) { hideAssigned = !!on; render(); }
|
||||||
|
function activeAxis() {
|
||||||
|
var tt = window.app.modules.targetTree;
|
||||||
|
return (tt && tt.activeAxis) ? tt.activeAxis() : 'tracking';
|
||||||
|
}
|
||||||
|
function fileDealtWith(file) {
|
||||||
|
var c = window.app.modules.classify;
|
||||||
|
var a = c.getAssignment(c.srcKeyForFile(file));
|
||||||
|
if (!a) return false;
|
||||||
|
if (a.excluded) return true;
|
||||||
|
return activeAxis() === 'transmittal' ? !!a.transmittalNodeId : !!a.trackingNodeId;
|
||||||
|
}
|
||||||
|
function subtreeRemaining(folder) {
|
||||||
|
var n = 0;
|
||||||
|
subtreeFiles(folder).forEach(function (f) { if (!fileDealtWith(f)) n++; });
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
// Hide a folder only when it's fully scanned (so we never hide one that may
|
||||||
|
// still reveal files) and nothing in its subtree remains to be dealt with.
|
||||||
|
function folderHidden(folder) {
|
||||||
|
if (!classifyOn() || !hideAssigned) return false;
|
||||||
|
if (folder.scanState && folder.scanState !== 'done') return false;
|
||||||
|
return subtreeRemaining(folder) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the folder tree
|
* Render the folder tree
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,6 +80,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
window.app.folderTree.forEach(folder => {
|
window.app.folderTree.forEach(folder => {
|
||||||
|
if (folderHidden(folder)) return;
|
||||||
const element = createFolderElement(folder);
|
const element = createFolderElement(folder);
|
||||||
container.appendChild(element);
|
container.appendChild(element);
|
||||||
});
|
});
|
||||||
|
|
@ -241,6 +273,7 @@
|
||||||
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 => {
|
||||||
|
if (folderHidden(child)) return;
|
||||||
const childElement = createFolderElement(child, level + 1);
|
const childElement = createFolderElement(child, level + 1);
|
||||||
childrenDiv.appendChild(childElement);
|
childrenDiv.appendChild(childElement);
|
||||||
});
|
});
|
||||||
|
|
@ -253,6 +286,7 @@
|
||||||
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) {
|
||||||
|
if (hideAssigned && fileDealtWith(file)) return;
|
||||||
filesDiv.appendChild(createFileElement(file, level + 1));
|
filesDiv.appendChild(createFileElement(file, level + 1));
|
||||||
});
|
});
|
||||||
div.appendChild(filesDiv);
|
div.appendChild(filesDiv);
|
||||||
|
|
@ -829,6 +863,7 @@
|
||||||
setupKeyboardShortcuts,
|
setupKeyboardShortcuts,
|
||||||
expandAll,
|
expandAll,
|
||||||
selectAll,
|
selectAll,
|
||||||
revealFile
|
revealFile,
|
||||||
|
setHideAssigned
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,15 @@
|
||||||
<input type="checkbox" id="autoScrollCheckbox" checked>
|
<input type="checkbox" id="autoScrollCheckbox" checked>
|
||||||
Auto-scroll
|
Auto-scroll
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label" id="hideCompliantLabel">
|
||||||
<input type="checkbox" id="hideCompliantCheckbox">
|
<input type="checkbox" id="hideCompliantCheckbox">
|
||||||
Hide Compliant
|
Hide Compliant
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox-label" id="hideAssignedLabel" hidden
|
||||||
|
title="Hide files already assigned in the active tab (or excluded), and folders left empty">
|
||||||
|
<input type="checkbox" id="hideAssignedCheckbox">
|
||||||
|
Hide Assigned
|
||||||
|
</label>
|
||||||
<span id="selectedFoldersCount" class="folder-count">0 folders selected</span>
|
<span id="selectedFoldersCount" class="folder-count">0 folders selected</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -532,3 +532,54 @@ test('deleting a tracking node clears the files placed in it', async ({ page })
|
||||||
}, FILE);
|
}, FILE);
|
||||||
expect(after).toBe('none');
|
expect(after).toBe('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('expandFolderPattern: alternation, zero-padded ranges, cartesian product', async ({ page }) => {
|
||||||
|
const r = await page.evaluate(() => {
|
||||||
|
const c = window.app.modules.classify;
|
||||||
|
return {
|
||||||
|
plain: c.expandFolderPattern('Plain'),
|
||||||
|
alt: c.expandFolderPattern('A-{PM,EL,EM}'),
|
||||||
|
range: c.expandFolderPattern('X-{0001-0002,0005}'),
|
||||||
|
full: c.expandFolderPattern('BMB-187023-{PM,EL,EM}-MOM-{0001-0002,0005}_A (IFR)'),
|
||||||
|
unbalanced: c.expandFolderPattern('Lit-{oops'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(r.plain).toEqual(['Plain']);
|
||||||
|
expect(r.alt).toEqual(['A-PM', 'A-EL', 'A-EM']);
|
||||||
|
expect(r.range).toEqual(['X-0001', 'X-0002', 'X-0005']);
|
||||||
|
expect(r.full.length).toBe(9);
|
||||||
|
expect(r.full[0]).toBe('BMB-187023-PM-MOM-0001_A (IFR)');
|
||||||
|
expect(r.full).toContain('BMB-187023-EM-MOM-0005_A (IFR)');
|
||||||
|
expect(r.unbalanced).toEqual(['Lit-{oops']); // unbalanced brace kept literal
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hide Assigned: hides files dealt-with on the active axis and folders left empty', async ({ page }) => {
|
||||||
|
await page.click('#modeClassifyBtn');
|
||||||
|
const before = await page.evaluate(() => {
|
||||||
|
const c = window.app.modules.classify;
|
||||||
|
c.reset();
|
||||||
|
const fA = { originalFilename: 'a1', extension: 'pdf', folderPath: 'A' };
|
||||||
|
window.app.folderTree = [
|
||||||
|
{ name: 'A', path: 'A', expanded: true, scanState: 'done', files: [fA], children: [] },
|
||||||
|
{ name: 'B', path: 'B', expanded: true, scanState: 'done',
|
||||||
|
files: [{ originalFilename: 'b1', extension: 'pdf', folderPath: 'B' }], children: [] },
|
||||||
|
];
|
||||||
|
const t = c.addTrackingNode(null, 'TN'); // assign A's file on the tracking axis
|
||||||
|
c.place([c.srcKeyForFile(fA)], t, 'tracking');
|
||||||
|
window.app.modules.tree.render();
|
||||||
|
return document.querySelectorAll('#folderTree .file-item').length;
|
||||||
|
});
|
||||||
|
expect(before).toBe(2); // nothing hidden yet
|
||||||
|
|
||||||
|
const after = await page.evaluate(() => {
|
||||||
|
window.app.modules.tree.setHideAssigned(true);
|
||||||
|
return {
|
||||||
|
files: Array.from(document.querySelectorAll('#folderTree .file-item .file-name')).map(e => e.textContent),
|
||||||
|
folderA: !!document.querySelector('#folderTree .folder-item[data-path="A"]'),
|
||||||
|
folderB: !!document.querySelector('#folderTree .folder-item[data-path="B"]'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(after.folderA).toBe(false); // A's only file is assigned on the active (tracking) axis → folder hidden
|
||||||
|
expect(after.folderB).toBe(true); // B still needs a tracking number → stays
|
||||||
|
expect(after.files).toEqual(['b1.pdf']);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue