From a8403d1f73d9f7cb69b2901ae1ac838ff82b7e57 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Tue, 9 Jun 2026 12:19:35 -0500 Subject: [PATCH] feat(classifier): mode toggle + dual-pane target trees (phase 2) Header gets a Rename / Classify & Copy switch. In Classify & Copy mode the spreadsheet pane is replaced by a tabbed target pane (By tracking number / By transmittal), while the source tree stays on the left. - target-tree.js: renders both trees from classify state; tracking-folder create/rename/delete (leaf folders styled as the revision); party CRUD + per-slot inline transmittal-bin form (date + TRN/SUB + seq + optional status/title); shows the derived filename + a validation badge for each placed file; live header stats (done / in progress / unassigned / excluded). - app.js setMode(): swaps panes, toggles classify mode, re-renders both trees. - 3 UI smoke tests added to classify.spec.js (12 total green). Co-Authored-By: Claude Opus 4.8 (1M context) --- classifier/build.sh | 1 + classifier/css/layout.css | 128 +++++++++++++++ classifier/js/app.js | 35 +++- classifier/js/target-tree.js | 301 +++++++++++++++++++++++++++++++++++ classifier/template.html | 37 ++++- tests/classify.spec.js | 44 ++++- 6 files changed, 542 insertions(+), 4 deletions(-) create mode 100644 classifier/js/target-tree.js diff --git a/classifier/build.sh b/classifier/build.sh index 0d1120b..d0bd5d0 100755 --- a/classifier/build.sh +++ b/classifier/build.sh @@ -56,6 +56,7 @@ concat_files \ "js/validator.js" \ "js/scanner.js" \ "js/tree.js" \ + "js/target-tree.js" \ "js/spreadsheet.js" \ "js/selection.js" \ "js/preview.js" \ diff --git a/classifier/css/layout.css b/classifier/css/layout.css index c8861d2..0b4d84d 100644 --- a/classifier/css/layout.css +++ b/classifier/css/layout.css @@ -265,6 +265,134 @@ margin-left: 1.5rem; } +/* ── Workflow mode switch (header) ─────────────────────────────────────── */ +.mode-switch { + display: inline-flex; + margin-left: 0.5rem; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +.mode-btn { + border: none; + background: var(--bg); + color: var(--text-muted); + padding: 0.3rem 0.7rem; + font-size: 0.8rem; + cursor: pointer; +} +.mode-btn + .mode-btn { border-left: 1px solid var(--border); } +.mode-btn.active { + background: var(--primary); + color: var(--bg); + font-weight: 600; +} + +/* ── Target pane (Classify & Copy) ─────────────────────────────────────── */ +.target-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} +.target-pane[hidden], .spreadsheet-pane[hidden] { display: none; } + +.target-tabs { display: flex; gap: 0.25rem; } +.target-tab { + border: 1px solid var(--border); + border-bottom: none; + background: var(--bg-secondary); + color: var(--text-muted); + padding: 0.3rem 0.8rem; + font-size: 0.85rem; + border-radius: var(--radius) var(--radius) 0 0; + cursor: pointer; +} +.target-tab.active { + background: var(--bg); + color: var(--primary); + font-weight: 600; +} + +.target-body { flex: 1; overflow: hidden; } +.target-panel { height: 100%; display: flex; flex-direction: column; } +.target-panel[hidden] { display: none; } +.target-panel__toolbar { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; +} +.target-hint { font-size: 0.75rem; color: var(--text-muted); } + +.target-tree { flex: 1; overflow: auto; padding: 0.5rem 0.75rem; } +.target-empty { color: var(--text-muted); font-size: 0.85rem; padding: 1rem 0.25rem; } + +/* tree nodes */ +.tnode { margin: 0.1rem 0; } +.tnode__children { margin-left: 1.25rem; border-left: 1px dashed var(--border); padding-left: 0.5rem; } +.tnode__row { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.2rem 0.3rem; + border-radius: var(--radius); +} +.tnode__row:hover { background: var(--bg-hover); } +.tnode__toggle { + border: none; background: none; cursor: pointer; + color: var(--text-muted); width: 1.1em; font-size: 0.8rem; padding: 0; +} +.tnode__icon { font-size: 0.85rem; } +.tnode__name { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.tnode--leaf > .tnode__row > .tnode__name { color: var(--primary); font-weight: 600; } +.tnode--party > .tnode__row > .tnode__name { font-weight: 700; } +.tnode--bin > .tnode__row > .tnode__name { color: var(--primary); } +.tnode__badge { + background: var(--primary); color: var(--bg); + 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; } +.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; + font-size: 0.72rem; padding: 0.05rem 0.35rem; color: var(--text); +} +.tnode__act:hover { background: var(--bg-hover); } + +/* placed files under a node */ +.tnode__files { margin: 0.1rem 0 0.2rem 1.6rem; } +.tfile { display: flex; align-items: baseline; gap: 0.4rem; font-size: 0.75rem; padding: 0.05rem 0; } +.tfile__orig { color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 14rem; } +.tfile__arrow { color: var(--text-muted); } +.tfile__name { color: var(--text); } +.tfile--err .tfile__name { color: var(--danger); } +.tfile--err::before { content: "⚠"; color: var(--danger); } + +/* transmittal slots + bin form */ +.tslot { margin: 0.15rem 0 0.15rem 1.1rem; } +.tslot__row { display: flex; align-items: center; gap: 0.5rem; padding: 0.15rem 0.3rem; } +.tslot__name { font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; } +.tnode--bin { margin-left: 1.1rem; } +.binform { + display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; + margin: 0.2rem 0 0.3rem 1.1rem; padding: 0.4rem; background: var(--bg-secondary); + border: 1px solid var(--border); border-radius: var(--radius); +} +.binform input, .binform select { + font-size: 0.78rem; padding: 0.2rem 0.3rem; + border: 1px solid var(--border); border-radius: var(--radius); + background: var(--bg); color: var(--text); +} +.binform__seq { width: 7rem; } +.binform__title { width: 11rem; } + +/* drop-target affordance (used in phase 3) */ +.tnode__row.drop-hover, .tslot.drop-hover { outline: 2px dashed var(--primary); outline-offset: -2px; background: var(--primary-light); } + /* Spreadsheet Pane */ .spreadsheet-pane { flex: 1; diff --git a/classifier/js/app.js b/classifier/js/app.js index 3538814..921b829 100644 --- a/classifier/js/app.js +++ b/classifier/js/app.js @@ -158,10 +158,36 @@ errorFiles: document.getElementById('errorFiles'), // Preview - togglePreviewBtn: document.getElementById('togglePreviewBtn') + togglePreviewBtn: document.getElementById('togglePreviewBtn'), + + // Mode switch + Classify & Copy panes + modeRenameBtn: document.getElementById('modeRenameBtn'), + modeClassifyBtn: document.getElementById('modeClassifyBtn'), + spreadsheetPane: document.getElementById('spreadsheetPane'), + targetPane: document.getElementById('targetPane') }; } + /** + * Switch between "Rename" (in-place grid) and "Classify & Copy" (map files + * onto target trees, copy renamed copies out). The source tree (left) stays + * in both modes; only the right pane swaps. + */ + function setMode(mode) { + const classify = mode === 'classify'; + app.dom.modeRenameBtn.classList.toggle('active', !classify); + app.dom.modeClassifyBtn.classList.toggle('active', classify); + if (app.dom.spreadsheetPane) app.dom.spreadsheetPane.hidden = classify; + if (app.dom.targetPane) app.dom.targetPane.hidden = !classify; + app.modules.classify.setEnabled(classify); + if (classify && app.modules.targetTree) { + app.modules.targetTree.init(); + app.modules.targetTree.render(); + } + // Re-render the source tree so its per-file markers appear/disappear. + if (app.modules.tree) app.modules.tree.render(); + } + /** * Set up event listeners */ @@ -188,6 +214,10 @@ // Collapse tree button app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree); + + // Workflow mode switch + if (app.dom.modeRenameBtn) app.dom.modeRenameBtn.addEventListener('click', function () { setMode('rename'); }); + if (app.dom.modeClassifyBtn) app.dom.modeClassifyBtn.addEventListener('click', function () { setMode('classify'); }); // Keyboard shortcuts document.addEventListener('keydown', handleKeyDown); @@ -325,7 +355,8 @@ app.modules.filter.init(); app.modules.sort.init(); app.modules.tree.setupKeyboardShortcuts(); - + if (app.modules.targetTree) app.modules.targetTree.init(); + // Now scan directory (this will trigger store updates and renders) await app.modules.scanner.scanDirectory(dirHandle); diff --git a/classifier/js/target-tree.js b/classifier/js/target-tree.js new file mode 100644 index 0000000..97245dc --- /dev/null +++ b/classifier/js/target-tree.js @@ -0,0 +1,301 @@ +/** + * ZDDC Classifier — target-tree pane (Classify & Copy mode). + * + * Renders the two orthogonal target trees the user maps files onto: + * - "By tracking number": folders that join with "-" into the tracking + * number; the leaf folder ("A (IFR)") is the revision+status. + * - "By transmittal": /{received,issued}/. + * + * Structure here, placements in classify.js. Drag-and-drop assignment is wired + * in source-dnd.js / phase 3; this module owns rendering + folder/bin CRUD and + * shows the derived filename for each placed file. + */ +(function () { + 'use strict'; + + var SLOTS = ['received', 'issued']; + + var els = {}; + var collapsed = {}; // nodeId -> true when collapsed (default expanded) + var openForm = null; // { partyId, slot } when a bin form is open + var initialized = false; + + function init() { + if (initialized) return; + initialized = true; + els = { + trackingTab: document.getElementById('trackingTab'), + transmittalTab: document.getElementById('transmittalTab'), + trackingPanel: document.getElementById('trackingPanel'), + transmittalPanel: document.getElementById('transmittalPanel'), + trackingTree: document.getElementById('trackingTree'), + transmittalTree: document.getElementById('transmittalTree'), + addTrackingRootBtn: document.getElementById('addTrackingRootBtn'), + addPartyBtn: document.getElementById('addPartyBtn'), + stats: document.getElementById('classifyStats'), + }; + + els.trackingTab.addEventListener('click', function () { showTab('tracking'); }); + els.transmittalTab.addEventListener('click', function () { showTab('transmittal'); }); + els.addTrackingRootBtn.addEventListener('click', function () { + var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ"):', ''); + if (name && name.trim()) C().addTrackingNode(null, name.trim()); + }); + els.addPartyBtn.addEventListener('click', function () { + var name = prompt('Party name (also the transmittal-number prefix):', ''); + if (name && name.trim()) C().addParty(name.trim()); + }); + + els.trackingTree.addEventListener('click', onTrackingClick); + els.transmittalTree.addEventListener('click', onTransmittalClick); + + C().on(render); + if (window.app.modules.store && window.app.modules.store.on) { + window.app.modules.store.on('files', render); + } + render(); + } + + function C() { return window.app.modules.classify; } + function allFiles() { + var s = window.app.modules.store; + return s && s.getAllFiles ? s.getAllFiles() : []; + } + + function showTab(which) { + var t = which === 'transmittal'; + els.trackingTab.classList.toggle('active', !t); + els.transmittalTab.classList.toggle('active', t); + els.trackingPanel.hidden = t; + els.transmittalPanel.hidden = !t; + } + + // ── render ─────────────────────────────────────────────────────────────── + function render() { + if (!initialized || !C().isEnabled()) return; + var files = allFiles(); + renderTrackingInto(els.trackingTree, C().getTrackingTree(), files); + renderTransmittalInto(els.transmittalTree, C().getTransmittalTree(), files); + renderStats(files); + } + + function renderStats(files) { + if (!els.stats) return; + var s = C().stats(files); + els.stats.textContent = s.done + ' done · ' + s.partial + ' in progress · ' + + s.none + ' unassigned · ' + s.excluded + ' excluded'; + } + + function el(tag, cls, text) { + var e = document.createElement(tag); + if (cls) e.className = cls; + if (text != null) e.textContent = text; + return e; + } + + function nodeActions(extra) { + var wrap = el('span', 'tnode__actions'); + (extra || []).forEach(function (a) { + var b = el('button', 'tnode__act', a.label); + b.dataset.act = a.act; + b.title = a.title || ''; + wrap.appendChild(b); + }); + return wrap; + } + + function fileList(files) { + var box = el('div', 'tnode__files'); + files.forEach(function (f) { + var d = C().deriveTarget(f); + var row = el('div', 'tfile' + (d.errors.length ? ' tfile--err' : '')); + row.title = d.errors.length ? d.errors.join('; ') : ''; + row.appendChild(el('span', 'tfile__orig', f.originalFilename + (f.extension ? '.' + f.extension : ''))); + row.appendChild(el('span', 'tfile__arrow', '→')); + row.appendChild(el('span', 'tfile__name', d.filename || '(incomplete)')); + box.appendChild(row); + }); + return box; + } + + // Tracking tree (recursive) + function renderTrackingInto(container, nodes, files) { + 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, files)); }); + } + function trackingNode(n, files) { + var isLeaf = (n.children || []).length === 0; + 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] ? '▸' : '▾')); + if (!isLeaf) toggle.dataset.act = 'toggle'; + row.appendChild(toggle); + row.appendChild(el('span', 'tnode__name', n.name)); + + var placed = C().filesInNode(n.id, 'tracking', files); + 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]) { + var kids = el('div', 'tnode__children'); + (n.children || []).forEach(function (c) { kids.appendChild(trackingNode(c, files)); }); + wrap.appendChild(kids); + } + return wrap; + } + + // Transmittal tree + function renderTransmittalInto(container, parties, files) { + container.textContent = ''; + if (!parties.length) { + container.appendChild(el('div', 'target-empty', 'No parties yet — “+ Party” to start.')); + return; + } + parties.forEach(function (p) { container.appendChild(partyNode(p, files)); }); + } + function partyNode(party, files) { + 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); + + SLOTS.forEach(function (slot) { + var slotNode = (party.children || []).filter(function (s) { return s.slot === slot; })[0]; + var sw = el('div', 'tslot'); + sw.dataset.party = party.id; + sw.dataset.slot = slot; + var sr = el('div', 'tslot__row'); + sr.appendChild(el('span', 'tslot__name', slot)); + var addBtn = el('button', 'tnode__act', '+ Transmittal'); + addBtn.dataset.act = 'addbin'; + sr.appendChild(addBtn); + sw.appendChild(sr); + + if (openForm && openForm.partyId === party.id && openForm.slot === slot) { + sw.appendChild(binForm(party.id, slot)); + } + (slotNode ? slotNode.children : []).forEach(function (bin) { + sw.appendChild(binNode(bin, files)); + }); + wrap.appendChild(sw); + }); + return wrap; + } + function binNode(bin, files) { + 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 = C().filesInNode(bin.id, 'transmittal', files); + 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; + } + + var STATUSES = ['---', 'IFA', 'IFB', 'IFC', 'IFD', 'IFI', 'IFP', 'IFR', 'IFU', 'REC', 'RSA', 'RSB', 'RSC', 'RSD', 'RSI', 'TBD']; + function binForm(partyId, slot) { + var form = el('div', 'binform'); + form.dataset.party = partyId; + form.dataset.slot = slot; + var date = el('input', 'binform__date'); date.type = 'date'; + try { date.value = new Date().toISOString().slice(0, 10); } catch (_) { /* ok */ } + var type = document.createElement('select'); type.className = 'binform__type'; + ['TRN', 'SUB'].forEach(function (t) { var o = el('option', null, t); o.value = t; type.appendChild(o); }); + var seq = el('input', 'binform__seq'); seq.type = 'text'; seq.placeholder = 'seq (e.g. 0007)'; + var status = document.createElement('select'); status.className = 'binform__status'; + STATUSES.forEach(function (s) { var o = el('option', null, s); o.value = s; status.appendChild(o); }); + var title = el('input', 'binform__title'); title.type = 'text'; title.placeholder = 'title (optional)'; + var add = el('button', 'btn btn-sm btn-primary', 'Add'); add.dataset.act = 'binadd'; + var cancel = el('button', 'btn btn-sm btn-secondary', 'Cancel'); cancel.dataset.act = 'bincancel'; + [date, type, seq, status, title, add, cancel].forEach(function (n) { form.appendChild(n); }); + return form; + } + + // ── events ───────────────────────────────────────────────────────────── + function closestNodeId(target) { + var n = target.closest('.tnode'); + return n ? n.dataset.id : null; + } + function onTrackingClick(e) { + var btn = e.target.closest('[data-act]'); + if (!btn) return; + var act = btn.dataset.act; + var id = closestNodeId(btn); + if (act === 'toggle') { collapsed[id] = !collapsed[id]; render(); return; } + if (act === 'add') { + var name = prompt('Child folder name (next tracking segment, or a leaf revision like "A (IFR)"):', ''); + if (name && name.trim()) C().addTrackingNode(id, name.trim()); + } else if (act === 'rename') { + var node = C().getNode(id); + var nn = prompt('Rename folder:', node ? node.name : ''); + if (nn && nn.trim()) C().renameNode(id, nn.trim()); + } else if (act === 'del') { + if (confirm('Delete this folder and everything under it? Files placed here become unassigned.')) C().deleteNode(id); + } + } + function onTransmittalClick(e) { + var btn = e.target.closest('[data-act]'); + if (!btn) return; + var act = btn.dataset.act; + + if (act === 'addbin') { + var slotEl = btn.closest('.tslot'); + openForm = { partyId: slotEl.dataset.party, slot: slotEl.dataset.slot }; + render(); + return; + } + if (act === 'bincancel') { openForm = null; render(); return; } + if (act === 'binadd') { + var form = btn.closest('.binform'); + var meta = { + date: form.querySelector('.binform__date').value, + type: form.querySelector('.binform__type').value, + seq: form.querySelector('.binform__seq').value.trim(), + status: form.querySelector('.binform__status').value, + title: form.querySelector('.binform__title').value.trim(), + }; + if (!meta.date || !meta.seq) { window.zddc.toast('Transmittal needs at least a date and a sequence number.', 'warning'); return; } + C().addTransmittalBin(form.dataset.party, form.dataset.slot, meta); + openForm = null; // render() fires from classify.notify() + return; + } + + var id = closestNodeId(btn); + if (act === 'rename-party') { + var node = C().getNode(id); + var nn = prompt('Rename party (re-derives its transmittal numbers):', node ? node.name : ''); + if (nn && nn.trim()) C().renameNode(id, nn.trim()); + } else if (act === 'del-party') { + if (confirm('Delete this party and all its transmittals? Files placed there become unassigned.')) C().deleteNode(id); + } else if (act === 'del') { + if (confirm('Delete this transmittal? Files placed here become unassigned.')) C().deleteNode(id); + } + } + + window.app.modules.targetTree = { + init: init, + render: render, + showTab: showTab, + }; +})(); diff --git a/classifier/template.html b/classifier/template.html index 70bfcd4..7c977db 100644 --- a/classifier/template.html +++ b/classifier/template.html @@ -30,6 +30,10 @@ +
+ + +
@@ -65,7 +69,7 @@ -
+

Files

@@ -126,6 +130,37 @@
+ +
+
+
+ + +
+
+ + | + +
+
+
+
+
+ + Folders join with “-” into the tracking number; the leaf folder is the revision — name it like “A (IFR)”. +
+
+
+ +
+
+
diff --git a/tests/classify.spec.js b/tests/classify.spec.js index 9e0e2ef..1732896 100644 --- a/tests/classify.spec.js +++ b/tests/classify.spec.js @@ -20,7 +20,13 @@ test.beforeEach(async ({ page }) => { await page.goto(PAGE, { waitUntil: 'load' }); const ok = await page.evaluate(() => !!(window.app && window.app.modules && window.app.modules.classify)); expect(ok).toBe(true); - await page.evaluate(() => window.app.modules.classify.reset()); + await page.evaluate(() => { + window.app.modules.classify.reset(); + // No directory is opened in these tests; dismiss the welcome overlay so + // it doesn't intercept clicks on the in-pane controls. + const w = document.getElementById('welcomeScreen'); + if (w) w.classList.add('hidden'); + }); }); // Build a tracking chain of folders and place one file in the deepest; @@ -135,6 +141,42 @@ test('exclude clears placements and reports excluded state', async ({ page }) => expect(r.d.excluded).toBe(true); }); +// ── Phase 2: mode toggle + target-tree rendering (UI) ────────────────────── + +test('mode switch swaps the spreadsheet pane for the target pane', async ({ page }) => { + await page.click('#modeClassifyBtn'); + expect(await page.locator('#targetPane').isHidden()).toBe(false); + expect(await page.locator('#spreadsheetPane').isHidden()).toBe(true); + await page.click('#modeRenameBtn'); + expect(await page.locator('#targetPane').isHidden()).toBe(true); + expect(await page.locator('#spreadsheetPane').isHidden()).toBe(false); +}); + +test('target tree renders structure and tabs switch', async ({ page }) => { + await page.click('#modeClassifyBtn'); + await page.evaluate(() => { + const c = window.app.modules.classify; + const acme = c.addTrackingNode(null, 'ACME-PROJ'); + c.addTrackingNode(acme, 'A (IFR)'); + const party = c.addParty('ClientCorp'); + c.addTransmittalBin(party, 'received', { date: '2026-03-14', type: 'TRN', seq: '0007' }); + }); + // Tracking panel visible by default with the nodes rendered. + await expect(page.locator('#trackingTree .tnode__name', { hasText: 'ACME-PROJ' })).toBeVisible(); + await expect(page.locator('#trackingTree .tnode--leaf .tnode__name', { hasText: 'A (IFR)' })).toBeVisible(); + // Switch to transmittal tab. + await page.click('#transmittalTab'); + expect(await page.locator('#transmittalPanel').isHidden()).toBe(false); + 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 }) => { + await page.click('#modeClassifyBtn'); + page.once('dialog', (d) => d.accept('ACME-PROJ')); + await page.click('#addTrackingRootBtn'); + await expect(page.locator('#trackingTree .tnode__name', { hasText: 'ACME-PROJ' })).toBeVisible(); +}); + test('deleting a tracking node clears the files placed in it', async ({ page }) => { const after = await page.evaluate((file) => { const c = window.app.modules.classify;