feat(classifier): three-state filters, expand/collapse-all, drop-prompt, preview + editable filenames
Classify & Copy interaction pass (replaces the single "Hide Assigned" toggle): - Source-tree filters: three "Show Unassigned / Show Assigned / Show Excluded" checkboxes (classify mode only) with live per-tab counts; "Hide Compliant" is now rename-mode only. Folders with nothing visible collapse out. - Target tree: ctrl/cmd-click a toggle to expand/collapse the whole subtree. - Tracking drop-to-any-level: dropping on a node that isn't already a complete leaf prompts for the remaining levels (e.g. "0001_0 (IFU)"), which are parsed and nested under the drop target. Dropping on a finished leaf assigns directly. - Placed-file rows: click to preview; the derived filename is now an inline input — edit it (full "TRACKING_REV (STATUS) - Title.ext") and the item is re-filed onto the parsed tracking path (created if needed) + title override. New classify helpers: trackingNodeComplete, trackingPathLabel. tree.setShowFilters replaces setHideAssigned. Tests updated/added (classify.spec.js -> 33 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
59ffd861f9
commit
139171481e
7 changed files with 233 additions and 48 deletions
|
|
@ -146,6 +146,10 @@
|
||||||
align-items: flex-end;
|
align-items: 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-filters .filter-count { color: var(--text-muted); font-size: 0.85em; }
|
||||||
|
|
||||||
.folder-stats,
|
.folder-stats,
|
||||||
.file-stats {
|
.file-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -478,7 +482,13 @@
|
||||||
|
|
||||||
/* placed-file row in the target pane is clickable (reveal in source) */
|
/* placed-file row in the target pane is clickable (reveal in source) */
|
||||||
.tfile { cursor: pointer; }
|
.tfile { cursor: pointer; }
|
||||||
.tfile:hover .tfile__name { text-decoration: underline; }
|
.tfile:hover .tfile__orig { text-decoration: underline; } /* click row (not the name input) → preview */
|
||||||
|
input.tfile__name {
|
||||||
|
flex: 1 1 auto; min-width: 10rem; font: inherit; color: var(--text);
|
||||||
|
border: 1px solid transparent; background: transparent; border-radius: 3px; padding: 0 0.2rem;
|
||||||
|
}
|
||||||
|
input.tfile__name:hover { border-color: var(--border); }
|
||||||
|
input.tfile__name:focus { border-color: var(--primary); background: var(--bg); outline: none; }
|
||||||
|
|
||||||
/* cross-tree reveal flash */
|
/* cross-tree reveal flash */
|
||||||
.reveal-flash, .match-highlight { animation: cl-flash 1.5s ease-out; }
|
.reveal-flash, .match-highlight { animation: cl-flash 1.5s ease-out; }
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,10 @@
|
||||||
sha256Checkbox: document.getElementById('sha256Checkbox'),
|
sha256Checkbox: document.getElementById('sha256Checkbox'),
|
||||||
hideCompliantCheckbox: document.getElementById('hideCompliantCheckbox'),
|
hideCompliantCheckbox: document.getElementById('hideCompliantCheckbox'),
|
||||||
hideCompliantLabel: document.getElementById('hideCompliantLabel'),
|
hideCompliantLabel: document.getElementById('hideCompliantLabel'),
|
||||||
hideAssignedCheckbox: document.getElementById('hideAssignedCheckbox'),
|
classifyFilters: document.getElementById('classifyFilters'),
|
||||||
hideAssignedLabel: document.getElementById('hideAssignedLabel'),
|
showUnassignedCheckbox: document.getElementById('showUnassignedCheckbox'),
|
||||||
|
showAssignedCheckbox: document.getElementById('showAssignedCheckbox'),
|
||||||
|
showExcludedCheckbox: document.getElementById('showExcludedCheckbox'),
|
||||||
|
|
||||||
// Folder tree
|
// Folder tree
|
||||||
folderTree: document.getElementById('folderTree'),
|
folderTree: document.getElementById('folderTree'),
|
||||||
|
|
@ -189,7 +191,7 @@
|
||||||
// Mode-specific source-tree filters: "Hide Compliant" is for the rename
|
// Mode-specific source-tree filters: "Hide Compliant" is for the rename
|
||||||
// grid; "Hide Assigned" is for the classify workflow.
|
// grid; "Hide Assigned" is for the classify workflow.
|
||||||
if (app.dom.hideCompliantLabel) app.dom.hideCompliantLabel.hidden = classify;
|
if (app.dom.hideCompliantLabel) app.dom.hideCompliantLabel.hidden = classify;
|
||||||
if (app.dom.hideAssignedLabel) app.dom.hideAssignedLabel.hidden = !classify;
|
if (app.dom.classifyFilters) app.dom.classifyFilters.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();
|
||||||
|
|
@ -223,14 +225,18 @@
|
||||||
// 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)
|
// Classify-mode source-tree filters: show/hide unassigned, assigned, excluded.
|
||||||
if (app.dom.hideAssignedCheckbox) {
|
function pushClassifyFilters() {
|
||||||
app.dom.hideAssignedCheckbox.addEventListener('change', function () {
|
if (app.modules.tree && app.modules.tree.setShowFilters) {
|
||||||
if (app.modules.tree && app.modules.tree.setHideAssigned) {
|
app.modules.tree.setShowFilters({
|
||||||
app.modules.tree.setHideAssigned(this.checked);
|
unassigned: app.dom.showUnassignedCheckbox.checked,
|
||||||
}
|
assigned: app.dom.showAssignedCheckbox.checked,
|
||||||
});
|
excluded: app.dom.showExcludedCheckbox.checked,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
[app.dom.showUnassignedCheckbox, app.dom.showAssignedCheckbox, app.dom.showExcludedCheckbox]
|
||||||
|
.forEach(function (cb) { if (cb) cb.addEventListener('change', pushClassifyFilters); });
|
||||||
|
|
||||||
// Collapse tree button
|
// Collapse tree button
|
||||||
app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree);
|
app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree);
|
||||||
|
|
|
||||||
|
|
@ -486,6 +486,23 @@
|
||||||
return cur;
|
return cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A tracking node is a "complete" drop target when it's a leaf whose name
|
||||||
|
// carries a valid "(STATUS)" — i.e. a file dropped there yields a full name
|
||||||
|
// with no more levels needed. Used to decide whether a drop should prompt.
|
||||||
|
function trackingNodeComplete(nodeId) {
|
||||||
|
var info = infoFor(nodeId);
|
||||||
|
if (!info || info.kind !== 'tracking') return false;
|
||||||
|
if ((info.node.children || []).length) return false;
|
||||||
|
var leaf = parseLeafLabel(info.node.name);
|
||||||
|
return !!(leaf.status && zddc.isValidStatus(leaf.status));
|
||||||
|
}
|
||||||
|
// Human-readable "root / … / node" path for a tracking node (prompt context).
|
||||||
|
function trackingPathLabel(nodeId) {
|
||||||
|
var info = infoFor(nodeId);
|
||||||
|
if (!info || info.kind !== 'tracking') return '';
|
||||||
|
return trackingChain(info).join(' / ');
|
||||||
|
}
|
||||||
|
|
||||||
// ── 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; }
|
||||||
|
|
@ -506,6 +523,7 @@
|
||||||
addTransmittalBin: addTransmittalBin, renameNode: renameNode, deleteNode: deleteNode,
|
addTransmittalBin: addTransmittalBin, renameNode: renameNode, deleteNode: deleteNode,
|
||||||
expandFolderPattern: expandFolderPattern,
|
expandFolderPattern: expandFolderPattern,
|
||||||
parseFolderLevels: parseFolderLevels, addTrackingPath: addTrackingPath,
|
parseFolderLevels: parseFolderLevels, addTrackingPath: addTrackingPath,
|
||||||
|
trackingNodeComplete: trackingNodeComplete, trackingPathLabel: trackingPathLabel,
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@
|
||||||
|
|
||||||
els.trackingTree.addEventListener('click', onTrackingClick);
|
els.trackingTree.addEventListener('click', onTrackingClick);
|
||||||
els.transmittalTree.addEventListener('click', onTransmittalClick);
|
els.transmittalTree.addEventListener('click', onTransmittalClick);
|
||||||
|
els.trackingTree.addEventListener('change', onFileNameChange);
|
||||||
|
els.transmittalTree.addEventListener('change', onFileNameChange);
|
||||||
|
|
||||||
setupDropZone(els.trackingTree, 'tracking');
|
setupDropZone(els.trackingTree, 'tracking');
|
||||||
setupDropZone(els.transmittalTree, 'transmittal');
|
setupDropZone(els.transmittalTree, 'transmittal');
|
||||||
|
|
@ -161,11 +163,18 @@
|
||||||
files.forEach(function (f) {
|
files.forEach(function (f) {
|
||||||
var d = C().deriveTarget(f);
|
var d = C().deriveTarget(f);
|
||||||
var row = el('div', 'tfile' + (d.errors.length ? ' tfile--err' : ''));
|
var row = el('div', 'tfile' + (d.errors.length ? ' tfile--err' : ''));
|
||||||
row.title = d.errors.length ? d.errors.join('; ') : 'Click to find this file in the source tree';
|
row.dataset.key = d.key;
|
||||||
row.dataset.key = d.key; // for cross-tree reveal
|
var orig = el('span', 'tfile__orig', f.originalFilename + (f.extension ? '.' + f.extension : ''));
|
||||||
row.appendChild(el('span', 'tfile__orig', f.originalFilename + (f.extension ? '.' + f.extension : '')));
|
orig.title = 'Click to preview';
|
||||||
|
row.appendChild(orig);
|
||||||
row.appendChild(el('span', 'tfile__arrow', '→'));
|
row.appendChild(el('span', 'tfile__arrow', '→'));
|
||||||
row.appendChild(el('span', 'tfile__name', d.filename || '(incomplete)'));
|
// Editable derived filename — edit it to re-file the item.
|
||||||
|
var name = el('input', 'tfile__name' + (d.errors.length ? ' tfile__name--err' : ''));
|
||||||
|
name.type = 'text';
|
||||||
|
name.value = d.filename || '';
|
||||||
|
name.placeholder = '(incomplete)';
|
||||||
|
name.title = d.errors.length ? d.errors.join('; ') : 'Edit this ZDDC filename to re-file the item';
|
||||||
|
row.appendChild(name);
|
||||||
box.appendChild(row);
|
box.appendChild(row);
|
||||||
});
|
});
|
||||||
return box;
|
return box;
|
||||||
|
|
@ -290,21 +299,65 @@
|
||||||
var n = target.closest('.tnode');
|
var n = target.closest('.tnode');
|
||||||
return n ? n.dataset.id : null;
|
return n ? n.dataset.id : null;
|
||||||
}
|
}
|
||||||
function revealInSource(e) {
|
function fileByKey(key) {
|
||||||
|
var files = allFiles();
|
||||||
|
for (var i = 0; i < files.length; i++) { if (C().srcKeyForFile(files[i]) === key) return files[i]; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Click a placed-file row (anywhere but its editable name) → preview it.
|
||||||
|
function previewFromTarget(e) {
|
||||||
|
if (e.target.closest('.tfile__name')) return false;
|
||||||
var tf = e.target.closest('.tfile');
|
var tf = e.target.closest('.tfile');
|
||||||
if (tf && tf.dataset.key && window.app.modules.tree.revealFile) {
|
if (!tf || !tf.dataset.key) return false;
|
||||||
window.app.modules.tree.revealFile(tf.dataset.key);
|
var f = fileByKey(tf.dataset.key);
|
||||||
return true;
|
if (f && window.app.modules.preview && window.app.modules.preview.previewFile) {
|
||||||
|
window.app.modules.preview.previewFile(f);
|
||||||
}
|
}
|
||||||
return false;
|
return true;
|
||||||
|
}
|
||||||
|
// Edited a placed-file's ZDDC filename → re-derive its tracking placement
|
||||||
|
// (creating the folder path if needed) + its title override.
|
||||||
|
function onFileNameChange(e) {
|
||||||
|
var input = e.target.closest('.tfile__name');
|
||||||
|
if (input) commitFilenameEdit(input);
|
||||||
|
}
|
||||||
|
function commitFilenameEdit(input) {
|
||||||
|
var tf = input.closest('.tfile');
|
||||||
|
if (!tf || !tf.dataset.key) return;
|
||||||
|
var parsed = window.zddc.parseFilename((input.value || '').trim());
|
||||||
|
if (!parsed || !parsed.valid) {
|
||||||
|
window.zddc.toast('Not a valid ZDDC filename — expected "TRACKING_REV (STATUS) - Title.ext".', 'warning');
|
||||||
|
render(); // restore the derived value
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var stem = parsed.trackingNumber + '_' + parsed.revision + ' (' + parsed.status + ')';
|
||||||
|
var leaf = C().addTrackingPath(null, C().parseFolderLevels(stem));
|
||||||
|
C().place([tf.dataset.key], leaf, 'tracking');
|
||||||
|
if (parsed.title != null) C().setTitleOverride(tf.dataset.key, parsed.title);
|
||||||
|
// place/setTitleOverride fire classify.notify → re-render.
|
||||||
|
}
|
||||||
|
// Collapse/expand a node and its whole subtree (ctrl/cmd-click a toggle).
|
||||||
|
function setSubtreeCollapsed(nodeId, collapse) {
|
||||||
|
var node = C().getNode(nodeId);
|
||||||
|
if (!node) return;
|
||||||
|
(function walk(n) {
|
||||||
|
if ((n.children || []).length) { if (collapse) collapsed[n.id] = true; else delete collapsed[n.id]; }
|
||||||
|
(n.children || []).forEach(walk);
|
||||||
|
})(node);
|
||||||
}
|
}
|
||||||
function onTrackingClick(e) {
|
function onTrackingClick(e) {
|
||||||
if (revealInSource(e)) return;
|
if (previewFromTarget(e)) return;
|
||||||
var btn = e.target.closest('[data-act]');
|
var btn = e.target.closest('[data-act]');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
var act = btn.dataset.act;
|
var act = btn.dataset.act;
|
||||||
var id = closestNodeId(btn);
|
var id = closestNodeId(btn);
|
||||||
if (act === 'toggle') { collapsed[id] = !collapsed[id]; render(); return; }
|
if (act === 'toggle') {
|
||||||
|
var collapse = !collapsed[id];
|
||||||
|
if (e.ctrlKey || e.metaKey) setSubtreeCollapsed(id, collapse);
|
||||||
|
else if (collapse) collapsed[id] = true; else delete 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)").\n'
|
var name = prompt('Child folder name (next tracking segment, or a leaf revision like "A (IFR)").\n'
|
||||||
+ 'Brace patterns expand: {PM,EL,EM}-MOM-{0001-0002,0005}_A (IFR)', '');
|
+ 'Brace patterns expand: {PM,EL,EM}-MOM-{0001-0002,0005}_A (IFR)', '');
|
||||||
|
|
@ -318,7 +371,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onTransmittalClick(e) {
|
function onTransmittalClick(e) {
|
||||||
if (revealInSource(e)) return;
|
if (previewFromTarget(e)) return;
|
||||||
var btn = e.target.closest('[data-act]');
|
var btn = e.target.closest('[data-act]');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
var act = btn.dataset.act;
|
var act = btn.dataset.act;
|
||||||
|
|
@ -391,10 +444,26 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var keys = window.app.modules.dnd.getDrag();
|
var keys = window.app.modules.dnd.getDrag();
|
||||||
window.app.modules.dnd.clearDrag();
|
window.app.modules.dnd.clearDrag();
|
||||||
if (keys.length) C().place(keys, t.id, axis);
|
if (!keys.length) return;
|
||||||
|
if (axis === 'tracking') placeTrackingDrop(keys, t.id);
|
||||||
|
else C().place(keys, t.id, axis);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tracking drop: if the target is already a complete leaf, assign directly;
|
||||||
|
// otherwise prompt for the remaining levels (parsed + nested under it) so a
|
||||||
|
// file can be dropped on an existing partial tracking number and completed.
|
||||||
|
function placeTrackingDrop(keys, nodeId) {
|
||||||
|
if (C().trackingNodeComplete(nodeId)) { C().place(keys, nodeId, 'tracking'); return; }
|
||||||
|
var label = C().trackingPathLabel(nodeId);
|
||||||
|
var input = prompt('Dropping under "' + label + '".\n'
|
||||||
|
+ 'Add the remaining tracking levels (e.g. "0001_0 (IFU)"), or leave blank to drop here:', '');
|
||||||
|
if (input === null) return; // cancelled
|
||||||
|
var levels = C().parseFolderLevels(input.trim());
|
||||||
|
var target = levels.length ? C().addTrackingPath(nodeId, levels) : nodeId;
|
||||||
|
C().place(keys, target, 'tracking');
|
||||||
|
}
|
||||||
|
|
||||||
// Reveal a source key's placement in the target pane (source → target).
|
// Reveal a source key's placement in the target pane (source → target).
|
||||||
function reveal(key) {
|
function reveal(key) {
|
||||||
var a = C().getAssignment(key);
|
var a = C().getAssignment(key);
|
||||||
|
|
|
||||||
|
|
@ -35,35 +35,61 @@
|
||||||
return dot;
|
return dot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── "Hide Assigned" filter (classify mode) ─────────────────────────────
|
// ── Classify-mode source-tree filters ──────────────────────────────────
|
||||||
// The goal in either target tab is to assign-or-exclude every file, so this
|
// The goal in either target tab is to assign-or-exclude every file. Each
|
||||||
// collapses the left tree down to only what's left to deal with on the
|
// file falls in one bucket FOR THE ACTIVE AXIS — unassigned / assigned /
|
||||||
// ACTIVE axis: hide files already assigned in the current tab (or excluded),
|
// excluded — and three "Show …" toggles control which buckets are visible
|
||||||
// and any folder whose whole (scanned) subtree is thereby empty.
|
// (so unchecking Assigned+Excluded leaves only what's left to do). A folder
|
||||||
var hideAssigned = false;
|
// whose whole scanned subtree is filtered away is itself hidden.
|
||||||
function setHideAssigned(on) { hideAssigned = !!on; render(); }
|
var showFilters = { unassigned: true, assigned: true, excluded: true };
|
||||||
|
function setShowFilters(f) {
|
||||||
|
showFilters = {
|
||||||
|
unassigned: f.unassigned !== false,
|
||||||
|
assigned: f.assigned !== false,
|
||||||
|
excluded: f.excluded !== false,
|
||||||
|
};
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function allFiltersOn() { return showFilters.unassigned && showFilters.assigned && showFilters.excluded; }
|
||||||
function activeAxis() {
|
function activeAxis() {
|
||||||
var tt = window.app.modules.targetTree;
|
var tt = window.app.modules.targetTree;
|
||||||
return (tt && tt.activeAxis) ? tt.activeAxis() : 'tracking';
|
return (tt && tt.activeAxis) ? tt.activeAxis() : 'tracking';
|
||||||
}
|
}
|
||||||
function fileDealtWith(file) {
|
// Bucket a file relative to the active axis: 'excluded' | 'assigned' | 'unassigned'.
|
||||||
|
function fileCategory(file) {
|
||||||
var c = window.app.modules.classify;
|
var c = window.app.modules.classify;
|
||||||
var a = c.getAssignment(c.srcKeyForFile(file));
|
var a = c.getAssignment(c.srcKeyForFile(file));
|
||||||
if (!a) return false;
|
if (a && a.excluded) return 'excluded';
|
||||||
if (a.excluded) return true;
|
var assigned = a && (activeAxis() === 'transmittal' ? a.transmittalNodeId : a.trackingNodeId);
|
||||||
return activeAxis() === 'transmittal' ? !!a.transmittalNodeId : !!a.trackingNodeId;
|
return assigned ? 'assigned' : 'unassigned';
|
||||||
}
|
}
|
||||||
function subtreeRemaining(folder) {
|
function fileVisible(file) { return !!showFilters[fileCategory(file)]; }
|
||||||
|
function subtreeVisibleCount(folder) {
|
||||||
var n = 0;
|
var n = 0;
|
||||||
subtreeFiles(folder).forEach(function (f) { if (!fileDealtWith(f)) n++; });
|
subtreeFiles(folder).forEach(function (f) { if (fileVisible(f)) n++; });
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
// Hide a folder only when it's fully scanned (so we never hide one that may
|
// 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.
|
// still reveal files) and the active filters leave nothing visible in it.
|
||||||
function folderHidden(folder) {
|
function folderHidden(folder) {
|
||||||
if (!classifyOn() || !hideAssigned) return false;
|
if (!classifyOn() || allFiltersOn()) return false;
|
||||||
if (folder.scanState && folder.scanState !== 'done') return false;
|
if (folder.scanState && folder.scanState !== 'done') return false;
|
||||||
return subtreeRemaining(folder) === 0;
|
return subtreeVisibleCount(folder) === 0;
|
||||||
|
}
|
||||||
|
// All scanned files (for the per-bucket counts on the filter checkboxes).
|
||||||
|
function allClassifyFiles() {
|
||||||
|
var out = [];
|
||||||
|
(window.app.folderTree || []).forEach(function (f) { subtreeFiles(f, out); });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function updateFilterCounts() {
|
||||||
|
if (!classifyOn()) return;
|
||||||
|
var n = { unassigned: 0, assigned: 0, excluded: 0 };
|
||||||
|
allClassifyFiles().forEach(function (f) { n[fileCategory(f)]++; });
|
||||||
|
['unassigned', 'assigned', 'excluded'].forEach(function (k) {
|
||||||
|
var el = document.getElementById('show' + k.charAt(0).toUpperCase() + k.slice(1) + 'Count');
|
||||||
|
if (el) el.textContent = '(' + n[k] + ')';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,6 +99,7 @@
|
||||||
const container = window.app.dom.folderTree;
|
const container = window.app.dom.folderTree;
|
||||||
wireClassifyInteractions();
|
wireClassifyInteractions();
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
updateFilterCounts();
|
||||||
|
|
||||||
if (window.app.folderTree.length === 0) {
|
if (window.app.folderTree.length === 0) {
|
||||||
container.innerHTML = '<div class="tree-empty">No folders found</div>';
|
container.innerHTML = '<div class="tree-empty">No folders found</div>';
|
||||||
|
|
@ -84,6 +111,9 @@
|
||||||
const element = createFolderElement(folder);
|
const element = createFolderElement(folder);
|
||||||
container.appendChild(element);
|
container.appendChild(element);
|
||||||
});
|
});
|
||||||
|
if (classifyOn() && !container.children.length) {
|
||||||
|
container.innerHTML = '<div class="tree-empty">Nothing matches the current filters.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +316,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;
|
if (classifyOn() && !fileVisible(file)) return;
|
||||||
filesDiv.appendChild(createFileElement(file, level + 1));
|
filesDiv.appendChild(createFileElement(file, level + 1));
|
||||||
});
|
});
|
||||||
div.appendChild(filesDiv);
|
div.appendChild(filesDiv);
|
||||||
|
|
@ -864,6 +894,6 @@
|
||||||
expandAll,
|
expandAll,
|
||||||
selectAll,
|
selectAll,
|
||||||
revealFile,
|
revealFile,
|
||||||
setHideAssigned
|
setShowFilters
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,20 @@
|
||||||
<input type="checkbox" id="hideCompliantCheckbox">
|
<input type="checkbox" id="hideCompliantCheckbox">
|
||||||
Hide Compliant
|
Hide Compliant
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label" id="hideAssignedLabel" hidden
|
<span id="classifyFilters" class="classify-filters" hidden>
|
||||||
title="Hide files already assigned in the active tab (or excluded), and folders left empty">
|
<label class="checkbox-label" title="Show files not yet assigned in the active tab">
|
||||||
<input type="checkbox" id="hideAssignedCheckbox">
|
<input type="checkbox" id="showUnassignedCheckbox" checked>
|
||||||
Hide Assigned
|
Show Unassigned <span class="filter-count" id="showUnassignedCount"></span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox-label" title="Show files already assigned in the active tab">
|
||||||
|
<input type="checkbox" id="showAssignedCheckbox" checked>
|
||||||
|
Show Assigned <span class="filter-count" id="showAssignedCount"></span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label" title="Show excluded files">
|
||||||
|
<input type="checkbox" id="showExcludedCheckbox" checked>
|
||||||
|
Show Excluded <span class="filter-count" id="showExcludedCount"></span>
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
<span id="selectedFoldersCount" class="folder-count">0 folders selected</span>
|
<span id="selectedFoldersCount" class="folder-count">0 folders selected</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -575,7 +575,7 @@ test('Hide Assigned: hides files dealt-with on the active axis and folders left
|
||||||
expect(before).toBe(2); // nothing hidden yet
|
expect(before).toBe(2); // nothing hidden yet
|
||||||
|
|
||||||
const after = await page.evaluate(() => {
|
const after = await page.evaluate(() => {
|
||||||
window.app.modules.tree.setHideAssigned(true);
|
window.app.modules.tree.setShowFilters({ unassigned: true, assigned: false, excluded: false });
|
||||||
return {
|
return {
|
||||||
files: Array.from(document.querySelectorAll('#folderTree .file-item .file-name')).map(e => e.textContent),
|
files: Array.from(document.querySelectorAll('#folderTree .file-item .file-name')).map(e => e.textContent),
|
||||||
folderA: !!document.querySelector('#folderTree .folder-item[data-path="A"]'),
|
folderA: !!document.querySelector('#folderTree .folder-item[data-path="A"]'),
|
||||||
|
|
@ -617,3 +617,46 @@ test('add-folder builds a nested chain sharing common ancestors', async ({ page
|
||||||
expect(r[0].children.map((n) => n.name)).toEqual(['0001', '0002']);
|
expect(r[0].children.map((n) => n.name)).toEqual(['0001', '0002']);
|
||||||
expect(r[0].children[0].children[0].name).toBe('0 (IFU)'); // leaf rev under each number
|
expect(r[0].children[0].children[0].name).toBe('0 (IFU)'); // leaf rev under each number
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('trackingNodeComplete: true only for a leaf with a valid status', async ({ page }) => {
|
||||||
|
const r = await page.evaluate(() => {
|
||||||
|
const c = window.app.modules.classify;
|
||||||
|
c.reset();
|
||||||
|
const parent = c.addTrackingNode(null, 'CPO');
|
||||||
|
const num = c.addTrackingNode(parent, '0001');
|
||||||
|
const leaf = c.addTrackingNode(num, '0 (IFU)');
|
||||||
|
const bare = c.addTrackingNode(c.addTrackingNode(null, 'X'), '0001'); // leaf, no status
|
||||||
|
return {
|
||||||
|
root: c.trackingNodeComplete(parent), // has children
|
||||||
|
num: c.trackingNodeComplete(num), // has a child leaf
|
||||||
|
leaf: c.trackingNodeComplete(leaf), // leaf + valid status
|
||||||
|
bare: c.trackingNodeComplete(bare), // leaf, no "(STATUS)"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(r).toEqual({ root: false, num: false, leaf: true, bare: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('editing a placed file’s filename re-files it onto the parsed tracking path', async ({ page }) => {
|
||||||
|
await page.click('#modeClassifyBtn');
|
||||||
|
const r = await page.evaluate(() => {
|
||||||
|
const c = window.app.modules.classify;
|
||||||
|
c.reset();
|
||||||
|
const leaf = c.addTrackingNode(c.addTrackingNode(null, 'OLD'), 'A (IFR)');
|
||||||
|
const file = { folderPath: 'Root/Sub', originalFilename: 'doc', extension: 'pdf' };
|
||||||
|
const key = c.srcKeyForFile(file);
|
||||||
|
c.place([key], leaf, 'tracking');
|
||||||
|
window.app.folderTree = [{
|
||||||
|
name: 'Sub', path: 'Sub', expanded: true, scanState: 'done', children: [], files: [file],
|
||||||
|
}];
|
||||||
|
window.app.modules.targetTree.render();
|
||||||
|
const input = document.querySelector('#trackingTree .tfile__name');
|
||||||
|
input.value = 'CPO-0002_0 (IFU) - New Title.pdf';
|
||||||
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
const d = c.deriveTarget(file);
|
||||||
|
return { tracking: d.tracking, revision: d.revision, status: d.status, title: d.title, complete: d.complete };
|
||||||
|
});
|
||||||
|
expect(r.tracking).toBe('CPO-0002');
|
||||||
|
expect(r.revision).toBe('0');
|
||||||
|
expect(r.status).toBe('IFU');
|
||||||
|
expect(r.title).toBe('New Title');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue