feat(classifier): By-tracking as a merged-cell table (pattern config + sticky)
The nested tracking tree wasted huge vertical space showing the hierarchy by
indentation. Render it as a spreadsheet-style table instead: each configured
field is a column, ancestor cells span (rowspan) their descendants' rows, the
revision gets its own aligned column, and each placed file is a row. Far denser,
and the hierarchy reads left-to-right.
- classify.js: a per-workspace pattern config { trackingFields[{name,optional}],
statuses, modifiers }, default ORIG·PROJ·DISC·TYPE·SEQ + optional SUFFIX,
seeded statuses from the shared enum. Serialized with the workspace; reset
keeps it (it's a setting, not data). getConfig/setConfig/getTrackingFields.
- target-tree.js: tree → flat rows → merged-cell <table>. A revision leaf is
detected by its "(STATUS)" suffix, so SEQ "0001" and revision "A (IFR)" land
in the right columns. Drop targets, CRUD, editable ZDDC name, validation,
filter and cross-reveal all preserved on the cells.
- css: sticky header + sticky merged-cell values (the value stays pinned under
the header while you scroll a tall group, so a span never reads blank).
- The original filename moves to a hover tooltip on the editable name.
Next: the revision context-menu (status / draft ~ / +Cn toggles + stepper) and
the settings UI to edit the pattern/status/modifier lists.
Tests: merged ancestors get the right rowspan; revisions align in one column;
the date revision stays intact (49 green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d8094bb9f
commit
1631e2b8ca
4 changed files with 274 additions and 55 deletions
|
|
@ -584,3 +584,49 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
|||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* ── By-tracking merged-cell table ──────────────────────────────────────── */
|
||||
#trackingTree { padding: 0; } /* table reaches the edges; cells carry padding */
|
||||
.ttable { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.82rem; }
|
||||
.ttable th, .ttable td {
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
padding: 0;
|
||||
}
|
||||
.ttable thead th {
|
||||
position: sticky; top: 0; z-index: 3;
|
||||
background: var(--bg-secondary, var(--bg));
|
||||
color: var(--text-muted);
|
||||
font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
||||
text-align: left; padding: 0.3rem 0.5rem; white-space: nowrap;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.ttable__rh { color: var(--primary); }
|
||||
.ttable__fileh { width: 99%; } /* the files column soaks up remaining width */
|
||||
.ttable__cell--empty { background: var(--bg-secondary, var(--bg)); }
|
||||
/* The merged-cell value stays pinned just under the header while you scroll the
|
||||
group, so a tall rowspan never reads as a blank column. */
|
||||
.tcell__inner {
|
||||
position: sticky; top: 1.6rem;
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
padding: 0.25rem 0.5rem; white-space: nowrap;
|
||||
}
|
||||
.tcell__name { font-weight: 600; }
|
||||
.trev__inner .tcell__name { color: var(--primary); }
|
||||
.ttable__cell:hover .tnode__actions, .ttable__rev:hover .tnode__actions { opacity: 1; }
|
||||
.ttable .drop-hover { outline: 2px solid var(--primary); outline-offset: -2px; }
|
||||
.ttable__file { padding: 0.1rem 0.4rem; }
|
||||
.ttable__drop { color: var(--text-muted); font-style: italic; font-size: 0.75rem; }
|
||||
.ttable .tfile { gap: 0.3rem; align-items: center; }
|
||||
.ttable .tfile__name {
|
||||
flex: 1; min-width: 8rem; max-width: 24rem;
|
||||
padding: 0.15rem 0.35rem; border: 1px solid transparent; border-radius: var(--radius);
|
||||
background: transparent; color: var(--text); font-size: 0.8rem;
|
||||
}
|
||||
.ttable .tfile__name:hover, .ttable .tfile__name:focus { border-color: var(--border); background: var(--bg); }
|
||||
.ttable .tfile__name--err { color: var(--danger); }
|
||||
.ttable .tfile--err::before { content: none; } /* we render our own badge instead */
|
||||
.tfile__badge { font-size: 0.78rem; flex: 0 0 auto; }
|
||||
.tfile__badge--ok { color: var(--success, #16a34a); }
|
||||
.tfile__badge--err { color: var(--danger); }
|
||||
|
|
|
|||
|
|
@ -29,6 +29,26 @@
|
|||
return 'n' + (Date.now().toString(36)) + '-' + (++_idSeq).toString(36);
|
||||
}
|
||||
|
||||
// Per-workspace tracking-number PATTERN config. Drives the By-tracking
|
||||
// table columns + (later) revision-modifier menus. Editable by the user.
|
||||
var DEFAULT_FIELDS = [
|
||||
{ name: 'ORIG', optional: false },
|
||||
{ name: 'PROJ', optional: false },
|
||||
{ name: 'DISC', optional: false },
|
||||
{ name: 'TYPE', optional: false },
|
||||
{ name: 'SEQ', optional: false },
|
||||
{ name: 'SUFFIX', optional: true },
|
||||
];
|
||||
var DEFAULT_STATUSES = (window.zddc && window.zddc.STATUSES) ? window.zddc.STATUSES.slice() : ['---'];
|
||||
var DEFAULT_MODIFIERS = ['B', 'C', 'N', 'Q'];
|
||||
function defaultConfig() {
|
||||
return {
|
||||
trackingFields: DEFAULT_FIELDS.map(function (f) { return { name: f.name, optional: !!f.optional }; }),
|
||||
statuses: DEFAULT_STATUSES.slice(),
|
||||
modifiers: DEFAULT_MODIFIERS.slice(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── state ────────────────────────────────────────────────────────────────
|
||||
var state = {
|
||||
enabled: false, // classify mode on/off
|
||||
|
|
@ -36,6 +56,7 @@
|
|||
trackingTree: [], // [ { id, name, children:[] } ] (leaf = no children)
|
||||
transmittalTree: [], // [ { id, kind:'party', name, children:[ slot ] } ]
|
||||
outputName: null, // remembered output directory display name
|
||||
config: defaultConfig(), // tracking-number pattern (fields/statuses/modifiers)
|
||||
};
|
||||
|
||||
// id -> { node, kind:'tracking'|'party'|'slot'|'transmittal', parent }
|
||||
|
|
@ -385,6 +406,7 @@
|
|||
trackingTree: state.trackingTree,
|
||||
transmittalTree: state.transmittalTree,
|
||||
outputName: state.outputName,
|
||||
config: state.config,
|
||||
};
|
||||
}
|
||||
function load(obj) {
|
||||
|
|
@ -393,9 +415,12 @@
|
|||
state.trackingTree = obj.trackingTree || [];
|
||||
state.transmittalTree = obj.transmittalTree || [];
|
||||
state.outputName = obj.outputName || null;
|
||||
state.config = normalizeConfig(obj.config);
|
||||
rebuildIndex();
|
||||
notify();
|
||||
}
|
||||
// Reset clears the CLASSIFICATION but keeps the pattern config — it's a
|
||||
// per-project setting, not part of the data being cleared.
|
||||
function reset() {
|
||||
state.assignments = {}; state.trackingTree = []; state.transmittalTree = [];
|
||||
state.outputName = null;
|
||||
|
|
@ -403,6 +428,23 @@
|
|||
notify();
|
||||
}
|
||||
|
||||
// ── pattern config ───────────────────────────────────────────────────────
|
||||
function normalizeConfig(c) {
|
||||
var d = defaultConfig();
|
||||
if (!c || typeof c !== 'object') return d;
|
||||
var fields = Array.isArray(c.trackingFields) && c.trackingFields.length
|
||||
? c.trackingFields.map(function (f) { return { name: String(f && f.name || '').trim() || '?', optional: !!(f && f.optional) }; })
|
||||
: d.trackingFields;
|
||||
return {
|
||||
trackingFields: fields,
|
||||
statuses: Array.isArray(c.statuses) && c.statuses.length ? c.statuses.slice() : d.statuses,
|
||||
modifiers: Array.isArray(c.modifiers) && c.modifiers.length ? c.modifiers.slice() : d.modifiers,
|
||||
};
|
||||
}
|
||||
function getConfig() { return state.config; }
|
||||
function getTrackingFields() { return state.config.trackingFields; }
|
||||
function setConfig(c) { state.config = normalizeConfig(c); notify(); }
|
||||
|
||||
// ── add-folder pattern expansion ─────────────────────────────────────────
|
||||
// Brace expansion for the add-folder box. Supports (non-nested) groups:
|
||||
// {a,b,c} → alternation: a | b | c
|
||||
|
|
@ -575,6 +617,7 @@
|
|||
trackingNodeComplete: trackingNodeComplete, trackingPathLabel: trackingPathLabel,
|
||||
transmittalRecord: transmittalRecord,
|
||||
findOrAddParty: findOrAddParty, findOrAddTransmittalBin: findOrAddTransmittalBin,
|
||||
getConfig: getConfig, setConfig: setConfig, getTrackingFields: getTrackingFields,
|
||||
getNode: getNode, getTrackingTree: function () { return state.trackingTree; },
|
||||
getTransmittalTree: function () { return state.transmittalTree; },
|
||||
// derive + reverse
|
||||
|
|
|
|||
|
|
@ -199,51 +199,153 @@
|
|||
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) { 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, 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;
|
||||
// ── By-tracking: merged-cell table ──────────────────────────────────────
|
||||
// The positional hierarchy reads left-to-right as columns (one per configured
|
||||
// field), ancestor cells span their descendants' rows, and the revision (the
|
||||
// leaf) gets its own aligned column. Each placed file is a row.
|
||||
|
||||
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 ? '·' : (expanded ? '▾' : '▸'));
|
||||
if (!isLeaf) toggle.dataset.act = 'toggle';
|
||||
row.appendChild(toggle);
|
||||
row.appendChild(el('span', 'tnode__name', n.name));
|
||||
if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length)));
|
||||
row.appendChild(nodeActions([
|
||||
{ act: 'add', label: '+', title: 'Add child folder' },
|
||||
// A node is a revision leaf when its name ends in a "(STATUS)" we recognise —
|
||||
// tracking field codes never carry a parenthesised status, so this cleanly
|
||||
// separates "0001" (a SEQ field) from "A (IFR)" (a revision).
|
||||
function revStatusOf(name) {
|
||||
var m = /\(\s*([A-Za-z0-9-]{1,5})\s*\)\s*$/.exec(name || '');
|
||||
return (m && window.zddc.isValidStatus(m[1])) ? m[1] : null;
|
||||
}
|
||||
function isRevisionLeaf(node) {
|
||||
return !(node.children || []).length && revStatusOf(node.name) != null;
|
||||
}
|
||||
// Flatten the tree into rows: { path:[fieldNodes], rev:revNode|null, file }.
|
||||
function buildTrackingRows(nodes, placedMap) {
|
||||
var rows = [];
|
||||
function emit(path, rev, files) {
|
||||
var fs = (files && files.length) ? files : [null];
|
||||
fs.forEach(function (f) { rows.push({ path: path, rev: rev, file: f }); });
|
||||
}
|
||||
function walk(node, ancestors) {
|
||||
var placed = placedMap[node.id] || [];
|
||||
if (isRevisionLeaf(node)) { emit(ancestors, node, placed); return; }
|
||||
var myPath = ancestors.concat(node); // node is a tracking field segment
|
||||
if (placed.length) emit(myPath, null, placed); // files dropped on a partial number
|
||||
var kids = node.children || [];
|
||||
if (kids.length) kids.forEach(function (c) { walk(c, myPath); });
|
||||
else if (!placed.length) emit(myPath, null, []); // empty leaf = drop target
|
||||
}
|
||||
nodes.forEach(function (n) { walk(n, []); });
|
||||
return rows;
|
||||
}
|
||||
function rowMatches(row) {
|
||||
if (!rfActive()) return true;
|
||||
if (row.file && fileRowMatches(row.file)) return true;
|
||||
if (row.rev && rfHit(row.rev.name)) return true;
|
||||
for (var i = 0; i < row.path.length; i++) { if (rfHit(row.path[i].name)) return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
function fieldCellContent(node) {
|
||||
var inner = el('div', 'tcell__inner');
|
||||
inner.appendChild(el('span', 'tcell__name', node.name));
|
||||
inner.appendChild(nodeActions([
|
||||
{ act: 'add', label: '+', title: 'Add child segment / revision' },
|
||||
{ act: 'rename', label: '✎', title: 'Rename' },
|
||||
{ act: 'del', label: '🗑', title: 'Delete' },
|
||||
]));
|
||||
wrap.appendChild(row);
|
||||
if (shownFiles.length) wrap.appendChild(fileList(shownFiles));
|
||||
if (!isLeaf && expanded && childEls.length) {
|
||||
var kids = el('div', 'tnode__children');
|
||||
childEls.forEach(function (ce) { kids.appendChild(ce); });
|
||||
wrap.appendChild(kids);
|
||||
return inner;
|
||||
}
|
||||
return wrap;
|
||||
function revCellContent(node, placedMap) {
|
||||
var inner = el('div', 'tcell__inner trev__inner');
|
||||
inner.appendChild(el('span', 'tcell__name', node.name));
|
||||
var n = (placedMap[node.id] || []).length;
|
||||
if (n) inner.appendChild(el('span', 'tnode__badge', String(n)));
|
||||
inner.appendChild(nodeActions([
|
||||
{ act: 'rename', label: '✎', title: 'Rename revision' },
|
||||
{ act: 'del', label: '🗑', title: 'Delete' },
|
||||
]));
|
||||
return inner;
|
||||
}
|
||||
// A placed-file cell: editable ZDDC name + validation badge; the original
|
||||
// filename is on hover, not shown inline. Reuses .tfile/.tfile__name so the
|
||||
// delegated preview + name-edit handlers apply.
|
||||
function fileCellContent(f) {
|
||||
var d = C().deriveTarget(f);
|
||||
var row = el('div', 'tfile' + (d.errors.length ? ' tfile--err' : ''));
|
||||
row.dataset.key = d.key;
|
||||
var orig = f.originalFilename + (f.extension ? '.' + f.extension : '');
|
||||
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('; ') + ' · ' : '') + 'original: ' + orig;
|
||||
row.appendChild(name);
|
||||
row.appendChild(el('span', 'tfile__badge' + (d.errors.length ? ' tfile__badge--err' : ' tfile__badge--ok'),
|
||||
d.errors.length ? '⚠' : '✓'));
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderTrackingInto(container, nodes, placedMap) {
|
||||
container.textContent = '';
|
||||
if (!nodes.length) {
|
||||
container.appendChild(el('div', 'target-empty', 'No tracking numbers yet — “+ Root folder” to start.'));
|
||||
return;
|
||||
}
|
||||
var rows = buildTrackingRows(nodes, placedMap).filter(rowMatches);
|
||||
if (!rows.length) {
|
||||
container.appendChild(el('div', 'target-empty', rfActive() ? 'No matches in the tracking tree.' : 'No tracking numbers yet.'));
|
||||
return;
|
||||
}
|
||||
var fields = C().getTrackingFields();
|
||||
var maxPath = rows.reduce(function (m, r) { return Math.max(m, r.path.length); }, 0);
|
||||
var nCols = Math.max(fields.length, maxPath);
|
||||
|
||||
function cellId(row, col) {
|
||||
if (col < nCols) { var n = row.path[col]; return n ? n.id : null; }
|
||||
return row.rev ? row.rev.id : null; // col === nCols → revision
|
||||
}
|
||||
// Rowspan run starting at row i for column col (0 = covered from above).
|
||||
function spanAt(col, i) {
|
||||
var id = cellId(rows[i], col);
|
||||
if (id == null) return 1;
|
||||
if (i > 0 && cellId(rows[i - 1], col) === id) return 0;
|
||||
var span = 1;
|
||||
for (var j = i + 1; j < rows.length; j++) { if (cellId(rows[j], col) === id) span++; else break; }
|
||||
return span;
|
||||
}
|
||||
|
||||
var table = el('table', 'ttable');
|
||||
var thead = el('thead'), htr = el('tr');
|
||||
for (var c = 0; c < nCols; c++) {
|
||||
htr.appendChild(el('th', 'ttable__fh', fields[c] ? fields[c].name + (fields[c].optional ? ' ?' : '') : '·'));
|
||||
}
|
||||
htr.appendChild(el('th', 'ttable__rh', 'REVISION'));
|
||||
htr.appendChild(el('th', 'ttable__fileh', 'Files'));
|
||||
thead.appendChild(htr); table.appendChild(thead);
|
||||
|
||||
var tbody = el('tbody');
|
||||
rows.forEach(function (row, i) {
|
||||
var tr = el('tr');
|
||||
for (var col = 0; col < nCols; col++) {
|
||||
var span = spanAt(col, i);
|
||||
if (span === 0) continue; // merged from the row above
|
||||
var node = row.path[col] || null;
|
||||
var td = el('td', 'ttable__cell' + (node ? '' : ' ttable__cell--empty'));
|
||||
if (span > 1) td.rowSpan = span;
|
||||
if (node) { td.dataset.id = node.id; td.appendChild(fieldCellContent(node)); }
|
||||
tr.appendChild(td);
|
||||
}
|
||||
var rspan = spanAt(nCols, i);
|
||||
if (rspan !== 0) {
|
||||
var rtd = el('td', 'ttable__rev' + (row.rev ? '' : ' ttable__cell--empty'));
|
||||
if (rspan > 1) rtd.rowSpan = rspan;
|
||||
if (row.rev) { rtd.dataset.id = row.rev.id; rtd.appendChild(revCellContent(row.rev, placedMap)); }
|
||||
tr.appendChild(rtd);
|
||||
}
|
||||
var ftd = el('td', 'ttable__file');
|
||||
if (row.file) ftd.appendChild(fileCellContent(row.file));
|
||||
else ftd.appendChild(el('span', 'ttable__drop', 'drop a file here'));
|
||||
tr.appendChild(ftd);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
container.appendChild(table);
|
||||
}
|
||||
|
||||
// Transmittal tree
|
||||
|
|
@ -334,7 +436,7 @@
|
|||
|
||||
// ── events ─────────────────────────────────────────────────────────────
|
||||
function closestNodeId(target) {
|
||||
var n = target.closest('.tnode');
|
||||
var n = target.closest('[data-id]');
|
||||
return n ? n.dataset.id : null;
|
||||
}
|
||||
function fileByKey(key) {
|
||||
|
|
@ -453,10 +555,14 @@
|
|||
// tracking → any folder node (.tnode)
|
||||
// transmittal → a transmittal bin only (.tnode--bin)
|
||||
function dropTarget(target, axis) {
|
||||
var sel = axis === 'transmittal' ? '.tnode--bin' : '.tnode';
|
||||
var node = target.closest(sel);
|
||||
if (!node || !node.dataset.id) return null;
|
||||
return { id: node.dataset.id, row: node.querySelector('.tnode__row') || node };
|
||||
if (axis === 'transmittal') {
|
||||
var bin = target.closest('.tnode--bin');
|
||||
if (!bin || !bin.dataset.id) return null;
|
||||
return { id: bin.dataset.id, row: bin.querySelector('.tnode__row') || bin };
|
||||
}
|
||||
var cell = target.closest('.ttable__cell[data-id], .ttable__rev[data-id]');
|
||||
if (!cell) return null;
|
||||
return { id: cell.dataset.id, row: cell };
|
||||
}
|
||||
function clearHover(container) {
|
||||
var hot = container.querySelectorAll('.drop-hover');
|
||||
|
|
@ -515,7 +621,7 @@
|
|||
}
|
||||
}
|
||||
function flashNode(container, id) {
|
||||
var node = container.querySelector('.tnode[data-id="' + id + '"]');
|
||||
var node = container.querySelector('[data-id="' + id + '"]');
|
||||
if (!node) return;
|
||||
node.scrollIntoView({ block: 'center' });
|
||||
var row = node.querySelector('.tnode__row') || node;
|
||||
|
|
|
|||
|
|
@ -161,9 +161,9 @@ test('target tree renders structure and tabs switch', async ({ page }) => {
|
|||
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();
|
||||
// Tracking panel visible by default with the table rendered.
|
||||
await expect(page.locator('#trackingTree .ttable__cell .tcell__name', { hasText: 'ACME-PROJ' })).toBeVisible();
|
||||
await expect(page.locator('#trackingTree .ttable__rev .tcell__name', { hasText: 'A (IFR)' })).toBeVisible();
|
||||
// Switch to transmittal tab.
|
||||
await page.click('#transmittalTab');
|
||||
expect(await page.locator('#transmittalPanel').isHidden()).toBe(false);
|
||||
|
|
@ -174,10 +174,10 @@ test('"+ Root folder" button (prompt) parses a name into nested levels', async (
|
|||
await page.click('#modeClassifyBtn');
|
||||
page.once('dialog', (d) => d.accept('CPO-0001_0 (IFU)'));
|
||||
await page.click('#addTrackingRootBtn');
|
||||
// "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();
|
||||
// "CPO-0001_0 (IFU)" → CPO / 0001 columns + "0 (IFU)" revision cell.
|
||||
await expect(page.locator('#trackingTree .tcell__name', { hasText: 'CPO' })).toBeVisible();
|
||||
await expect(page.locator('#trackingTree .tcell__name', { hasText: '0001' })).toBeVisible();
|
||||
await expect(page.locator('#trackingTree .ttable__rev .tcell__name', { hasText: '0 (IFU)' })).toBeVisible();
|
||||
});
|
||||
|
||||
// ── Phase 3: drag-and-drop assignment (drop handler) ───────────────────────
|
||||
|
|
@ -188,7 +188,7 @@ test('dropping a file onto a tracking leaf assigns it', async ({ page }) => {
|
|||
const c = window.app.modules.classify;
|
||||
const leaf = c.addTrackingNode(c.addTrackingNode(null, 'ACME-MECH-0001'), 'A (IFR)');
|
||||
window.app.modules.targetTree.render();
|
||||
const row = document.querySelector('#trackingTree .tnode--leaf .tnode__row');
|
||||
const row = document.querySelector('#trackingTree .ttable__rev[data-id]');
|
||||
const key = 'Sub/foundation.pdf';
|
||||
window.app.modules.dnd.setDrag([key]);
|
||||
row.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true }));
|
||||
|
|
@ -739,7 +739,7 @@ test('tracking-tree filter reveals matching nodes and hides the rest', async ({
|
|||
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);
|
||||
return Array.from(document.querySelectorAll('#trackingTree .tcell__name')).map((e) => e.textContent);
|
||||
});
|
||||
expect(names).toContain('CPO');
|
||||
expect(names).toContain('0001');
|
||||
|
|
@ -960,3 +960,27 @@ test('a fully-excluded folder is struck through like its files', async ({ page }
|
|||
expect(r.before).toBe(false); // not struck through while active
|
||||
expect(r.after).toBe(true); // struck through once the whole subtree is excluded
|
||||
});
|
||||
|
||||
test('By-tracking table merges shared ancestors and aligns revisions', async ({ page }) => {
|
||||
await page.click('#modeClassifyBtn');
|
||||
const r = await page.evaluate(() => {
|
||||
const c = window.app.modules.classify, tt = window.app.modules.targetTree;
|
||||
c.reset();
|
||||
c.addTrackingPath(null, c.parseFolderLevels('LKU-123456-PM-SCH-0001_2025-11-17 (IFI)'));
|
||||
c.addTrackingPath(null, c.parseFolderLevels('LKU-123456-PM-SCH-0001_A (IFR)'));
|
||||
c.addTrackingPath(null, c.parseFolderLevels('CPO-0001_0 (IFU)'));
|
||||
tt.render();
|
||||
const cellByName = (n) => Array.from(document.querySelectorAll('#trackingTree .ttable__cell .tcell__name'))
|
||||
.filter((e) => e.textContent === n).map((e) => e.closest('td'))[0];
|
||||
const lku = cellByName('LKU'), cpo = cellByName('CPO');
|
||||
return {
|
||||
lkuSpan: lku ? lku.rowSpan : 0,
|
||||
cpoSpan: cpo ? cpo.rowSpan : 0,
|
||||
revs: Array.from(document.querySelectorAll('#trackingTree .ttable__rev .tcell__name')).map((e) => e.textContent),
|
||||
};
|
||||
});
|
||||
expect(r.lkuSpan).toBe(2); // the LKU ancestor cell spans its two revisions (merged)
|
||||
expect(r.cpoSpan).toBe(1);
|
||||
// The revisions live in one aligned column; the date revision stays intact.
|
||||
expect(r.revs).toEqual(['2025-11-17 (IFI)', 'A (IFR)', '0 (IFU)']);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue