fix(classifier): parse add-folder names into nested levels; controls back to right/hover
Follow-up to the Classify & Copy add-folder work: - Add-folder now parses each (brace-expanded) name into the nested tracking levels it represents — split on "-", then the FINAL "_" splits the leaf revision. "CPO-0001_0 (IFU)" → CPO / 0001 / 0 (IFU); a braced pattern nests every expansion and shares common ancestors. New classify.parseFolderLevels + addTrackingPath (ensure-path with name reuse). - Node add/edit/delete controls moved back to the RIGHT of the level name and revealed on hover (was left + always-visible). Tests: parseFolderLevels cases + a nested-chain/shared-ancestor test; updated the "+ Root folder" test for the new nesting (classify.spec.js -> 31 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f839fc0c9
commit
055f4cf4e0
4 changed files with 88 additions and 13 deletions
|
|
@ -405,9 +405,9 @@
|
|||
background: var(--primary); color: var(--bg);
|
||||
border-radius: 999px; padding: 0 0.4rem; font-size: 0.7rem; font-weight: 600;
|
||||
}
|
||||
/* Node CRUD controls sit to the LEFT of the level name (always visible) so
|
||||
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; }
|
||||
/* Node CRUD controls sit to the right of the level name, revealed on hover. */
|
||||
.tnode__actions { display: inline-flex; gap: 0.1rem; margin-left: 0.3rem; flex: 0 0 auto; opacity: 0; transition: opacity 0.12s; }
|
||||
.tnode__row:hover .tnode__actions, .tslot__row:hover .tnode__actions { opacity: 1; }
|
||||
.tnode__act {
|
||||
border: 1px solid var(--border); background: var(--bg);
|
||||
border-radius: var(--radius); cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -449,6 +449,43 @@
|
|||
return results.map(function (r) { return r.trim(); }).filter(Boolean);
|
||||
}
|
||||
|
||||
// Parse one (already brace-expanded) folder name into the nested tracking
|
||||
// levels it represents: split on "-" into tracking-number segments, then
|
||||
// split the FINAL segment once on "_" to separate the last tracking segment
|
||||
// from the "REV (STATUS)" leaf. So "CPO-0001_0 (IFU)" → ["CPO","0001","0 (IFU)"]
|
||||
// and "BMB-187023-PM-MOM-0001_A (IFR)" → ["BMB","187023","PM","MOM","0001","A (IFR)"].
|
||||
// A name with no "-"/"_" is a single level (e.g. adding a leaf "A (IFR)").
|
||||
function parseFolderLevels(name) {
|
||||
var s = String(name == null ? '' : name).trim();
|
||||
if (!s) return [];
|
||||
var segs = s.split('-');
|
||||
var last = segs.pop();
|
||||
var u = last.indexOf('_');
|
||||
if (u >= 0) { segs.push(last.slice(0, u)); segs.push(last.slice(u + 1)); }
|
||||
else { segs.push(last); }
|
||||
return segs.map(function (x) { return x.trim(); }).filter(Boolean);
|
||||
}
|
||||
// Children array for a tracking node (or the roots for null), or null.
|
||||
function trackingChildren(parentId) {
|
||||
if (!parentId) return state.trackingTree;
|
||||
var info = infoFor(parentId);
|
||||
return (info && info.kind === 'tracking') ? info.node.children : null;
|
||||
}
|
||||
// Ensure a nested chain of tracking folders exists under parentId, reusing
|
||||
// an existing child when one already has that name (so sibling leaves share
|
||||
// ancestors). Returns the leaf node id.
|
||||
function addTrackingPath(parentId, segments) {
|
||||
var cur = parentId || null;
|
||||
(segments || []).forEach(function (seg) {
|
||||
var name = (seg || '').trim();
|
||||
if (!name) return;
|
||||
var kids = trackingChildren(cur) || [];
|
||||
var existing = kids.filter(function (n) { return n.name === name; })[0];
|
||||
cur = existing ? existing.id : addTrackingNode(cur, name);
|
||||
});
|
||||
return cur;
|
||||
}
|
||||
|
||||
// ── mode ─────────────────────────────────────────────────────────────────
|
||||
function setEnabled(on) { state.enabled = !!on; notify(); }
|
||||
function isEnabled() { return state.enabled; }
|
||||
|
|
@ -468,6 +505,7 @@
|
|||
addTrackingNode: addTrackingNode, addParty: addParty,
|
||||
addTransmittalBin: addTransmittalBin, renameNode: renameNode, deleteNode: deleteNode,
|
||||
expandFolderPattern: expandFolderPattern,
|
||||
parseFolderLevels: parseFolderLevels, addTrackingPath: addTrackingPath,
|
||||
getNode: getNode, getTrackingTree: function () { return state.trackingTree; },
|
||||
getTransmittalTree: function () { return state.transmittalTree; },
|
||||
// derive + reverse
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
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); });
|
||||
// Each expanded name is parsed into nested tracking levels (split on
|
||||
// "-", final "_" splits the leaf rev), reusing shared ancestors.
|
||||
names.forEach(function (nm) { C().addTrackingPath(parentId, C().parseFolderLevels(nm)); });
|
||||
}
|
||||
|
||||
// ── render ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -187,15 +189,16 @@
|
|||
var toggle = el('button', 'tnode__toggle', isLeaf ? '·' : (collapsed[n.id] ? '▸' : '▾'));
|
||||
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' },
|
||||
]));
|
||||
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);
|
||||
|
||||
if (placed.length) wrap.appendChild(fileList(placed));
|
||||
|
|
@ -221,11 +224,11 @@
|
|||
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' },
|
||||
]));
|
||||
row.appendChild(el('span', 'tnode__name', party.name));
|
||||
wrap.appendChild(row);
|
||||
|
||||
SLOTS.forEach(function (slot) {
|
||||
|
|
@ -254,10 +257,10 @@
|
|||
var wrap = el('div', 'tnode tnode--bin');
|
||||
wrap.dataset.id = bin.id;
|
||||
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)'));
|
||||
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));
|
||||
return wrap;
|
||||
|
|
|
|||
|
|
@ -170,11 +170,14 @@ test('target tree renders structure and tabs switch', async ({ page }) => {
|
|||
await expect(page.locator('#transmittalTree .tnode--bin .tnode__name', { hasText: 'ClientCorp-TRN-0007' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('"+ Root folder" button (prompt) adds a tracking node', async ({ page }) => {
|
||||
test('"+ Root folder" button (prompt) parses a name into nested levels', async ({ page }) => {
|
||||
await page.click('#modeClassifyBtn');
|
||||
page.once('dialog', (d) => d.accept('ACME-PROJ'));
|
||||
page.once('dialog', (d) => d.accept('CPO-0001_0 (IFU)'));
|
||||
await page.click('#addTrackingRootBtn');
|
||||
await expect(page.locator('#trackingTree .tnode__name', { hasText: 'ACME-PROJ' })).toBeVisible();
|
||||
// "CPO-0001_0 (IFU)" → CPO / 0001 / 0 (IFU) (three nested levels).
|
||||
await expect(page.locator('#trackingTree .tnode__name', { hasText: 'CPO' })).toBeVisible();
|
||||
await expect(page.locator('#trackingTree .tnode__name', { hasText: '0001' })).toBeVisible();
|
||||
await expect(page.locator('#trackingTree .tnode__name', { hasText: '0 (IFU)' })).toBeVisible();
|
||||
});
|
||||
|
||||
// ── Phase 3: drag-and-drop assignment (drop handler) ───────────────────────
|
||||
|
|
@ -583,3 +586,34 @@ test('Hide Assigned: hides files dealt-with on the active axis and folders left
|
|||
expect(after.folderB).toBe(true); // B still needs a tracking number → stays
|
||||
expect(after.files).toEqual(['b1.pdf']);
|
||||
});
|
||||
|
||||
test('parseFolderLevels: split by - then a final _ into nested levels', async ({ page }) => {
|
||||
const r = await page.evaluate(() => {
|
||||
const c = window.app.modules.classify;
|
||||
return {
|
||||
three: c.parseFolderLevels('CPO-0001_0 (IFU)'),
|
||||
full: c.parseFolderLevels('BMB-187023-PM-MOM-0001_A (IFR)'),
|
||||
leafOnly: c.parseFolderLevels('A (IFR)'),
|
||||
noRev: c.parseFolderLevels('CPO-0001'),
|
||||
};
|
||||
});
|
||||
expect(r.three).toEqual(['CPO', '0001', '0 (IFU)']);
|
||||
expect(r.full).toEqual(['BMB', '187023', 'PM', 'MOM', '0001', 'A (IFR)']);
|
||||
expect(r.leafOnly).toEqual(['A (IFR)']);
|
||||
expect(r.noRev).toEqual(['CPO', '0001']);
|
||||
});
|
||||
|
||||
test('add-folder builds a nested chain sharing common ancestors', async ({ page }) => {
|
||||
const r = await page.evaluate(() => {
|
||||
const c = window.app.modules.classify;
|
||||
c.reset();
|
||||
// Brace-expand then nest: two leaves under shared CPO/000x ancestors.
|
||||
c.expandFolderPattern('CPO-{0001,0002}_0 (IFU)').forEach((nm) =>
|
||||
c.addTrackingPath(null, c.parseFolderLevels(nm)));
|
||||
return JSON.parse(JSON.stringify(c.getTrackingTree(), (k, v) => (k === 'id' ? undefined : v)));
|
||||
});
|
||||
expect(r.length).toBe(1); // one shared CPO root
|
||||
expect(r[0].name).toBe('CPO');
|
||||
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
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue