feat(classifier): MDL becomes a read-only catalog (MDL ∪ archive) overlay

Reframes the By-MDL tab as a "⊞ Catalog" button that opens an overlay over the
target pane (the left filetree stays the drag source). The catalog is the
de-duplicated union of everything the project knows about a tracking number:
its MDL deliverables AND its archive files, merged by tracking number, with an
informational "Archive revs" column (which revisions already exist) and an "MDL"
flag. Nothing is written or altered — the Revision column is classifier-local
and starts blank (never pre-filled from an old archive rev).

- Drag a source file onto a row → assigns the tracking number only (the mdl
  axis); set the revision in the bulk-editable Revision column (ctrl-shift
  select rows + ctrl-Enter). Per-file Title: MDL/file toggle + ✕ remove kept.
- Columns split the tracking number into the configured pattern fields, each with
  its own autofilter (per-column, via the shared seltable). Default pattern is
  now the 8-field ORIG-PHASE-PROJECT-AREA-DISC-TYPE-SEQ-SUFFIX.
- Server load merges every party's archive/<party>/mdl/*.yaml with a recursive
  walk of the archive documents; local load reads a folder of deliverable yamls.

Test: catalog shows merged archive revisions; drop names a file (tracking only);
bulk revision feeds the derived name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-11 14:58:31 -05:00
parent 93f1eb8d63
commit d4d48cad4a
5 changed files with 153 additions and 79 deletions

View file

@ -571,8 +571,24 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
cursor: wait; cursor: wait;
} }
/* ── By-MDL panel (seltable rows = deliverable drop targets) ─────────────── */ /* ── Catalog overlay (MDL archive; seltable rows = drop targets) ───────── */
#mdlPanel .seltable { height: 100%; } .target-pane { position: relative; }
.target-tabs__catalog { margin-left: 0.75rem; }
.catalog-overlay {
position: absolute; inset: 0; z-index: 20;
display: flex; flex-direction: column; min-height: 0;
background: var(--bg); border-left: 2px solid var(--primary);
}
.catalog-overlay[hidden] { display: none; }
.catalog-overlay__head {
display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
padding: 0.45rem 0.75rem; border-bottom: 1px solid var(--border); background: var(--bg-secondary, var(--bg));
}
.catalog-overlay__title { font-weight: 700; font-size: 0.9rem; }
.catalog-overlay__close { margin-left: auto; background: none; border: none; font-size: 1.5rem; line-height: 1; color: var(--text-muted); cursor: pointer; padding: 0 0.4rem; }
.catalog-overlay__close:hover { color: var(--text); }
.catalog-overlay__table { flex: 1; min-height: 0; }
.catalog-overlay__table .seltable { height: 100%; }
.mdl-rev__input { .mdl-rev__input {
width: 8rem; padding: 0.15rem 0.35rem; border: 1px solid var(--border); width: 8rem; padding: 0.15rem 0.35rem; border: 1px solid var(--border);
border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 0.8rem; border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 0.8rem;

View file

@ -33,7 +33,9 @@
// table columns + (later) revision-modifier menus. Editable by the user. // table columns + (later) revision-modifier menus. Editable by the user.
var DEFAULT_FIELDS = [ var DEFAULT_FIELDS = [
{ name: 'ORIG', optional: false }, { name: 'ORIG', optional: false },
{ name: 'PROJ', optional: false }, { name: 'PHASE', optional: false },
{ name: 'PROJECT', optional: false },
{ name: 'AREA', optional: false },
{ name: 'DISC', optional: false }, { name: 'DISC', optional: false },
{ name: 'TYPE', optional: false }, { name: 'TYPE', optional: false },
{ name: 'SEQ', optional: false }, { name: 'SEQ', optional: false },
@ -488,6 +490,8 @@
return { return {
id: r.id || uid(), party: r.party || '', id: r.id || uid(), party: r.party || '',
trackingNumber: r.trackingNumber || '', title: r.title || '', trackingNumber: r.trackingNumber || '', title: r.title || '',
inMdl: !!r.inMdl,
archiveRevisions: Array.isArray(r.archiveRevisions) ? r.archiveRevisions : [],
revisionCell: r.revisionCell || '', revisionCell: r.revisionCell || '',
}; };
}); });

View file

@ -19,8 +19,9 @@
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' | 'mdl' — the active axis var currentTab = 'tracking'; // 'tracking' | 'transmittal' — the active tab
var mdlTable = null; // the seltable controller for the By-MDL panel var catalogOpen = false; // the Catalog overlay (the 'mdl' axis) is open
var mdlTable = null; // the seltable controller for the catalog
var mdlPlaced = {}; // latest placed.mdl map (read by the placed-file cell) var mdlPlaced = {}; // latest placed.mdl map (read by the placed-file cell)
function init() { function init() {
@ -29,7 +30,7 @@
els = { els = {
trackingTab: document.getElementById('trackingTab'), trackingTab: document.getElementById('trackingTab'),
transmittalTab: document.getElementById('transmittalTab'), transmittalTab: document.getElementById('transmittalTab'),
mdlTab: document.getElementById('mdlTab'), catalogBtn: document.getElementById('catalogBtn'),
trackingPanel: document.getElementById('trackingPanel'), trackingPanel: document.getElementById('trackingPanel'),
transmittalPanel: document.getElementById('transmittalPanel'), transmittalPanel: document.getElementById('transmittalPanel'),
mdlPanel: document.getElementById('mdlPanel'), mdlPanel: document.getElementById('mdlPanel'),
@ -37,6 +38,7 @@
transmittalTree: document.getElementById('transmittalTree'), transmittalTree: document.getElementById('transmittalTree'),
mdlTree: document.getElementById('mdlTree'), mdlTree: document.getElementById('mdlTree'),
loadMdlBtn: document.getElementById('loadMdlBtn'), loadMdlBtn: document.getElementById('loadMdlBtn'),
catalogCloseBtn: document.getElementById('catalogCloseBtn'),
addTrackingRootBtn: document.getElementById('addTrackingRootBtn'), addTrackingRootBtn: document.getElementById('addTrackingRootBtn'),
addPartyBtn: document.getElementById('addPartyBtn'), addPartyBtn: document.getElementById('addPartyBtn'),
stats: document.getElementById('classifyStats'), stats: document.getElementById('classifyStats'),
@ -44,7 +46,8 @@
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'); });
if (els.mdlTab) els.mdlTab.addEventListener('click', function () { showTab('mdl'); }); if (els.catalogBtn) els.catalogBtn.addEventListener('click', function () { catalogOpen ? closeCatalog() : openCatalog(); });
if (els.catalogCloseBtn) els.catalogCloseBtn.addEventListener('click', closeCatalog);
if (els.loadMdlBtn) els.loadMdlBtn.addEventListener('click', loadMdl); if (els.loadMdlBtn) els.loadMdlBtn.addEventListener('click', loadMdl);
els.addTrackingRootBtn.addEventListener('click', function () { els.addTrackingRootBtn.addEventListener('click', function () {
var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ").\n' var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ").\n'
@ -98,18 +101,31 @@
} }
function showTab(which) { function showTab(which) {
currentTab = (which === 'transmittal' || which === 'mdl') ? which : 'tracking'; currentTab = which === 'transmittal' ? 'transmittal' : 'tracking';
els.trackingTab.classList.toggle('active', currentTab === 'tracking'); els.trackingTab.classList.toggle('active', currentTab === 'tracking');
els.transmittalTab.classList.toggle('active', currentTab === 'transmittal'); els.transmittalTab.classList.toggle('active', currentTab === 'transmittal');
if (els.mdlTab) els.mdlTab.classList.toggle('active', currentTab === 'mdl');
els.trackingPanel.hidden = currentTab !== 'tracking'; els.trackingPanel.hidden = currentTab !== 'tracking';
els.transmittalPanel.hidden = currentTab !== 'transmittal'; els.transmittalPanel.hidden = currentTab !== 'transmittal';
if (els.mdlPanel) els.mdlPanel.hidden = currentTab !== 'mdl';
// The source-tree Show filters are per-axis, so the visible set changes // The source-tree Show filters are per-axis, so the visible set changes
// with the active tab — re-render the left tree. // with the active tab — re-render the left tree.
if (window.app.modules.tree && window.app.modules.tree.render) window.app.modules.tree.render(); if (window.app.modules.tree && window.app.modules.tree.render) window.app.modules.tree.render();
} }
function activeAxis() { return currentTab === 'transmittal' ? 'transmittal' : currentTab === 'mdl' ? 'mdl' : 'tracking'; } // The active axis is the catalog ('mdl') while the overlay is open, else the tab.
function activeAxis() { return catalogOpen ? 'mdl' : (currentTab === 'transmittal' ? 'transmittal' : 'tracking'); }
function reRenderSource() { if (window.app.modules.tree && window.app.modules.tree.render) window.app.modules.tree.render(); }
function openCatalog() {
catalogOpen = true;
if (els.mdlPanel) els.mdlPanel.hidden = false;
if (els.catalogBtn) els.catalogBtn.classList.add('active');
render();
reRenderSource();
}
function closeCatalog() {
catalogOpen = false;
if (els.mdlPanel) els.mdlPanel.hidden = true;
if (els.catalogBtn) els.catalogBtn.classList.remove('active');
reRenderSource();
}
// Expand a brace pattern into folder names and create them (confirming a // Expand a brace pattern into folder names and create them (confirming a
// multi-create first). parentId null = root folders. See expandFolderPattern. // multi-create first). parentId null = root folders. See expandFolderPattern.
@ -474,7 +490,7 @@
if (!C().getMdlList().length) { if (!C().getMdlList().length) {
mdlTable = null; mdlTable = null;
els.mdlTree.textContent = ''; els.mdlTree.textContent = '';
els.mdlTree.appendChild(el('div', 'target-empty', 'No MDL loaded — “Load MDL…” to bring a projects deliverables in as drop targets.')); els.mdlTree.appendChild(el('div', 'target-empty', 'Nothing loaded yet — “Load…” to pull in the projects MDL deliverables and archive tracking numbers.'));
return; return;
} }
ensureMdlTable(); ensureMdlTable();
@ -483,31 +499,35 @@
function ensureMdlTable() { function ensureMdlTable() {
if (mdlTable) return mdlTable; if (mdlTable) return mdlTable;
var c = C(); var c = C();
// One column per configured tracking-number field (split positionally),
// then Title, MDL (✓), Archive revisions (informational), and the editable
// classifier-local Revision. Each column has its own autofilter.
var cols = c.getTrackingFields().map(function (f, i) {
return { key: 'f' + i, title: f.name, get: function (r) { return (r.trackingNumber || '').split('-')[i] || ''; } };
});
cols.push({ key: 'title', title: 'Title', get: function (r) { return r.title || ''; } });
cols.push({ key: 'mdl', title: 'MDL', cls: 'catalog-mdl', get: function (r) { return r.inMdl ? '✓' : ''; } });
cols.push({ key: 'arev', title: 'Archive revs', get: function (r) { return (r.archiveRevisions || []).join(', '); } });
cols.push({
key: 'rev', title: 'Revision', cls: 'mdl-rev', get: function (r) { return r.revisionCell; },
render: function (r, td) {
var inp = document.createElement('input');
inp.type = 'text'; inp.className = 'mdl-rev__input'; inp.value = r.revisionCell || '';
inp.placeholder = 'A (IFR)'; inp.setAttribute('data-no-select', '');
inp.addEventListener('change', function () { c.setRevisionCell(r.id, inp.value.trim()); });
td.appendChild(inp);
},
});
mdlTable = window.app.modules.seltable.create({ mdlTable = window.app.modules.seltable.create({
container: els.mdlTree, container: els.mdlTree,
filterPlaceholder: 'Filter deliverables by tracking number, title, party…',
extraTitle: 'Files', extraTitle: 'Files',
rows: function () { return c.getMdlList(); }, rows: function () { return c.getMdlList(); },
rowId: function (r) { return r.id; }, rowId: function (r) { return r.id; },
columns: [ columns: cols,
{ key: 'tracking', title: 'Tracking number', get: function (r) { return r.trackingNumber; } },
{ key: 'title', title: 'Title', get: function (r) { return r.title; } },
{ key: 'party', title: 'Party', get: function (r) { return r.party; } },
{
key: 'rev', title: 'Revision', cls: 'mdl-rev', get: function (r) { return r.revisionCell; },
render: function (r, td) {
var inp = document.createElement('input');
inp.type = 'text'; inp.className = 'mdl-rev__input'; inp.value = r.revisionCell || '';
inp.placeholder = 'A (IFR)'; inp.setAttribute('data-no-select', '');
inp.addEventListener('change', function () { c.setRevisionCell(r.id, inp.value.trim()); });
td.appendChild(inp);
},
},
],
onRowDrop: function (rowId, keys) { c.place(keys, rowId, 'mdl'); }, onRowDrop: function (rowId, keys) { c.place(keys, rowId, 'mdl'); },
onActivate: function (ids) { onActivate: function (ids) {
if (!ids.length) return; if (!ids.length) return;
var v = prompt('Set the revision on ' + ids.length + ' selected deliverable(s) (e.g. "A (IFR)"):', ''); var v = prompt('Set the revision on ' + ids.length + ' selected row(s) (e.g. "A (IFR)"):', '');
if (v != null) c.setRevisionCells(ids, v.trim()); if (v != null) c.setRevisionCells(ids, v.trim());
}, },
rowExtra: function (r, td) { renderMdlPlaced(r, td); }, rowExtra: function (r, td) { renderMdlPlaced(r, td); },
@ -538,22 +558,13 @@
}); });
} }
// Load deliverables into the MDL table — a local folder of .yaml or a server // Load the catalog: the union of the project's MDL deliverables and its
// project (one or more parties). Each yaml's filename stem is the tracking // archive tracking numbers, deduped by tracking number. Server reads both;
// number; the revision cell starts blank (classifier-local). // a local folder reads just its *.yaml deliverables. Writes/alters nothing —
function yamlToRow(party, stem, obj) { // the revision cell is classifier-local and starts blank.
return { function isRowYaml(nm) { return /\.yaml$/i.test(nm) && nm !== 'table.yaml' && nm !== 'form.yaml'; }
id: party + '|' + stem,
party: party,
trackingNumber: stem,
title: (obj && obj.title) || '',
revisionCell: '',
};
}
async function loadMdl() { async function loadMdl() {
if (window.zddc && window.zddc.source && location.protocol !== 'file:') { if (window.zddc && window.zddc.source && location.protocol !== 'file:') return loadMdlServer();
return loadMdlServer();
}
return loadMdlLocal(); return loadMdlLocal();
} }
async function loadMdlLocal() { async function loadMdlLocal() {
@ -564,10 +575,11 @@
var rows = []; var rows = [];
try { try {
for await (var entry of dir.values()) { for await (var entry of dir.values()) {
if (entry.kind !== 'file' || !/\.yaml$/i.test(entry.name) || entry.name === 'table.yaml' || entry.name === 'form.yaml') continue; var nm = String(entry.name).replace(/\/$/, '');
var txt = await (await entry.getFile()).text(); if (entry.kind !== 'file' || !isRowYaml(nm)) continue;
var obj = null; try { obj = window.jsyaml.load(txt); } catch (_) { /* skip bad yaml */ } var obj = null; try { obj = window.jsyaml.load(await (await entry.getFile()).text()); } catch (_) { /* skip */ }
rows.push(yamlToRow(dir.name || 'local', entry.name.replace(/\.yaml$/i, ''), obj)); var stem = nm.replace(/\.yaml$/i, '');
rows.push({ id: stem, party: dir.name || 'local', trackingNumber: stem, title: (obj && obj.title) || '', inMdl: true, archiveRevisions: [], revisionCell: '' });
} }
} catch (e) { window.zddc.toast('Reading the MDL folder failed — ' + (e.message || e), 'error'); return; } } catch (e) { window.zddc.toast('Reading the MDL folder failed — ' + (e.message || e), 'error'); return; }
finishLoad(rows); finishLoad(rows);
@ -581,29 +593,63 @@
if (!proj) return; if (!proj) return;
var rel = (proj.url || ('/' + proj.name + '/')); if (rel.charAt(rel.length - 1) !== '/') rel += '/'; var rel = (proj.url || ('/' + proj.name + '/')); if (rel.charAt(rel.length - 1) !== '/') rel += '/';
var archive = new src.HttpDirectoryHandle(new URL(rel + 'archive/', location.origin).href, 'archive'); var archive = new src.HttpDirectoryHandle(new URL(rel + 'archive/', location.origin).href, 'archive');
var rows = []; var byTn = Object.create(null);
function ensure(tn) { return byTn[tn] || (byTn[tn] = { tracking: tn, title: '', inMdl: false, party: '', revs: Object.create(null) }); }
window.zddc.toast('Scanning the project MDL + archive…', 'info', { durationMs: 4000 });
try { try {
for await (var party of archive.values()) { for await (var party of archive.values()) {
if (party.kind !== 'directory') continue; if (party.kind !== 'directory') continue;
var pn = String(party.name).replace(/\/$/, ''); var pn = String(party.name).replace(/\/$/, '');
if (pn.charAt(0) === '.' || pn.charAt(0) === '_') continue; if (pn.charAt(0) === '.' || pn.charAt(0) === '_') continue;
var mdlDir; for await (var slot of party.values()) {
try { mdlDir = await party.getDirectoryHandle('mdl'); } catch (e) { continue; } // no mdl for this party if (slot.kind !== 'directory') continue;
for await (var entry of mdlDir.values()) { var sn = String(slot.name).replace(/\/$/, '');
var nm = String(entry.name).replace(/\/$/, ''); if (sn.charAt(0) === '.' || sn.charAt(0) === '_' || sn === 'rsk') continue;
if (entry.kind !== 'file' || !/\.yaml$/i.test(nm) || nm === 'table.yaml' || nm === 'form.yaml') continue; if (sn === 'mdl') { // registered deliverables
var obj = null; for await (var ye of slot.values()) {
try { obj = window.jsyaml.load(await (await entry.getFile()).text()); } catch (_) { /* skip */ } var ynm = String(ye.name).replace(/\/$/, '');
rows.push(yamlToRow(pn, nm.replace(/\.yaml$/i, ''), obj)); if (ye.kind !== 'file' || !isRowYaml(ynm)) continue;
var obj = null; try { obj = window.jsyaml.load(await (await ye.getFile()).text()); } catch (_) { /* skip */ }
var row = ensure(ynm.replace(/\.yaml$/i, ''));
row.inMdl = true; if (!row.title && obj && obj.title) row.title = obj.title; if (!row.party) row.party = pn;
}
} else { // archive documents → tracking + revisions
await walkArchiveInto(slot, ensure, pn);
}
} }
} }
} catch (e) { window.zddc.toast('Reading the project MDL failed — ' + (e.message || e), 'error'); return; } } catch (e) { window.zddc.toast('Reading the project failed — ' + (e.message || e), 'error'); return; }
var rows = Object.keys(byTn).map(function (tn) {
var x = byTn[tn];
return { id: tn, party: x.party, trackingNumber: tn, title: x.title, inMdl: x.inMdl, archiveRevisions: Object.keys(x.revs).sort(), revisionCell: '' };
});
finishLoad(rows); finishLoad(rows);
} }
// Recursively collect ZDDC-named archive files under a slot → tracking +
// the set of revisions seen for each.
async function walkArchiveInto(dirH, ensure, party) {
for await (var entry of dirH.values()) {
var nm = String(entry.name).replace(/\/$/, '');
if (entry.kind === 'directory') {
if (nm.charAt(0) === '.' || nm.charAt(0) === '_') continue;
await walkArchiveInto(await dirH.getDirectoryHandle(nm), ensure, party);
} else {
var p = window.zddc.parseFilename(nm);
if (p && p.valid && p.trackingNumber) {
var row = ensure(p.trackingNumber);
if (!row.title) row.title = p.title || '';
if (!row.party) row.party = party;
row.revs[(p.revision + (p.status ? ' (' + p.status + ')' : '')).trim()] = true;
}
}
}
}
function finishLoad(rows) { function finishLoad(rows) {
C().setMdlList(rows); C().setMdlList(rows);
showTab('mdl'); openCatalog();
window.zddc.toast(rows.length ? ('Loaded ' + rows.length + ' deliverable' + (rows.length === 1 ? '' : 's') + ' — set revisions and drag files on.') : 'No deliverables found.', rows.length ? 'success' : 'warning'); window.zddc.toast(rows.length
? ('Catalog: ' + rows.length + ' tracking number' + (rows.length === 1 ? '' : 's') + ' (MDL archive). Filter, drag files on, set revisions.')
: 'Nothing found in the MDL or archive.', rows.length ? 'success' : 'warning');
} }
// ── events ───────────────────────────────────────────────────────────── // ── events ─────────────────────────────────────────────────────────────
@ -808,8 +854,8 @@
var a = C().getAssignment(key); var a = C().getAssignment(key);
if (!a) return; if (!a) return;
if (a.mdlNodeId) { if (a.mdlNodeId) {
showTab('mdl'); openCatalog();
if (mdlTable) { mdlTable.setFilter(''); mdlTable.renderBody(); } if (mdlTable) { mdlTable.renderBody(); }
} else if (a.trackingNodeId) { } else if (a.trackingNodeId) {
showTab('tracking'); collapsed = {}; render(); showTab('tracking'); collapsed = {}; render();
flashNode(els.trackingTree, a.trackingNodeId); flashNode(els.trackingTree, a.trackingNodeId);

View file

@ -163,7 +163,7 @@
<div class="target-tabs" role="tablist"> <div class="target-tabs" role="tablist">
<button class="target-tab active" id="trackingTab" role="tab">By tracking number</button> <button class="target-tab active" id="trackingTab" role="tab">By tracking number</button>
<button class="target-tab" id="transmittalTab" role="tab">By transmittal</button> <button class="target-tab" id="transmittalTab" role="tab">By transmittal</button>
<button class="target-tab" id="mdlTab" role="tab">By MDL</button> <button id="catalogBtn" class="btn btn-secondary btn-sm target-tabs__catalog" title="Open the catalog — everything the project knows: MDL deliverables archive tracking numbers. Drag files onto a row to name them.">⊞ Catalog</button>
</div> </div>
<div class="pane-header-right"> <div class="pane-header-right">
<span id="classifyStats" class="file-stats"></span> <span id="classifyStats" class="file-stats"></span>
@ -195,14 +195,20 @@
placeholder="Filter the transmittal tree…" aria-label="Filter transmittal tree"> placeholder="Filter the transmittal tree…" aria-label="Filter transmittal tree">
<div id="transmittalTree" class="target-tree"></div> <div id="transmittalTree" class="target-tree"></div>
</section> </section>
<section id="mdlPanel" class="target-panel" hidden>
<div class="target-panel__toolbar">
<button id="loadMdlBtn" class="btn btn-sm btn-secondary">⊞ Load MDL…</button>
<span class="target-hint">Deliverables become drop targets — set a revision, then drag files on. Ctrl-shift select rows + ctrl-Enter to set a revision on many at once.</span>
</div>
<div id="mdlTree" class="target-tree"></div>
</section>
</div> </div>
<!-- Catalog overlay (everything the project knows): opened by the
Catalog button. Covers the target pane; the left filetree stays
the drag source. Read-only on the MDL/archive; the Revision column
is classifier-local. -->
<section id="mdlPanel" class="catalog-overlay" hidden>
<div class="catalog-overlay__head">
<span class="catalog-overlay__title">Catalog — MDL archive</span>
<button id="loadMdlBtn" class="btn btn-sm btn-secondary">⊞ Load…</button>
<span class="target-hint">Drag files onto a row to assign its tracking number; set the Revision column (ctrl-shift select + ctrl-Enter to set many at once).</span>
<button id="catalogCloseBtn" class="catalog-overlay__close" title="Close">&times;</button>
</div>
<div id="mdlTree" class="catalog-overlay__table"></div>
</section>
</main> </main>
</div> </div>

View file

@ -1311,7 +1311,7 @@ test('classify: an MDL placement names a file; revision from the cell, transmitt
expect(r.fileTitleName).toContain('messy scan 47'); // title toggle → the file's own title expect(r.fileTitleName).toContain('messy scan 47'); // title toggle → the file's own title
}); });
test('By MDL tab: drop a file on a deliverable row names it; bulk revision applies', async ({ page }) => { test('Catalog: shows archive revs, drop on a row names the file, bulk revision applies', async ({ page }) => {
await page.click('#modeClassifyBtn'); await page.click('#modeClassifyBtn');
const r = await page.evaluate(() => { const r = await page.evaluate(() => {
const c = window.app.modules.classify, tt = window.app.modules.targetTree; const c = window.app.modules.classify, tt = window.app.modules.targetTree;
@ -1319,20 +1319,22 @@ test('By MDL tab: drop a file on a deliverable row names it; bulk revision appli
const f = { originalFilename: 'scan', extension: 'pdf', folderPath: 'R' }; const f = { originalFilename: 'scan', extension: 'pdf', folderPath: 'R' };
window.app.folderTree = [{ name: 'R', path: 'R', files: [f], children: [] }]; window.app.folderTree = [{ name: 'R', path: 'R', files: [f], children: [] }];
const key = c.srcKeyForFile(f); const key = c.srcKeyForFile(f);
// Catalog rows = MDL archive merged: one MDL+archive row, one archive-only.
c.setMdlList([ c.setMdlList([
{ id: 'm1', party: 'ACM', trackingNumber: 'ACM-PRJ-EL-SPC-0001', title: 'Spec' }, { id: 'm1', party: 'ACM', trackingNumber: 'ACM-PRJ-EL-SPC-0001', title: 'Spec', inMdl: true, archiveRevisions: ['A (IFR)', 'B (IFC)'] },
{ id: 'm2', party: 'ACM', trackingNumber: 'ACM-PRJ-EL-SPC-0002', title: 'Spec 2' }, { id: 'm2', party: 'ACM', trackingNumber: 'ACM-PRJ-EL-SPC-0002', title: 'Spec 2', inMdl: false, archiveRevisions: ['0 (IFC)'] },
]); ]);
tt.showTab('mdl'); tt.render(); tt.render(); // builds the catalog seltable into #mdlTree
const row = document.querySelector('#mdlTree .seltable__row[data-id="m1"]'); const row = document.querySelector('#mdlTree .seltable__row[data-id="m1"]');
const hasRow = !!row; const archiveRevsShown = !!row && row.textContent.includes('A (IFR)') && row.textContent.includes('B (IFC)');
window.app.modules.dnd.setDrag([key]); window.app.modules.dnd.setDrag([key]);
row.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true })); // drop the file on m1 row.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true })); // drop the file on m1
const placed = (c.getAssignment(key) || {}).mdlNodeId; const placed = (c.getAssignment(key) || {}).mdlNodeId;
c.setRevisionCells(['m1', 'm2'], 'A (IFR)'); // ctrl-enter bulk path c.setRevisionCells(['m1', 'm2'], 'A (IFR)'); // ctrl-Enter bulk path
return { hasRow, placed, named: c.deriveTarget(f).filename }; return { hasRow: !!row, archiveRevsShown, placed, named: c.deriveTarget(f).filename };
}); });
expect(r.hasRow).toBe(true); expect(r.hasRow).toBe(true);
expect(r.placed).toBe('m1'); // drop placed the file on the deliverable expect(r.archiveRevsShown).toBe(true); // merged archive revisions shown (informational)
expect(r.named).toBe('ACM-PRJ-EL-SPC-0001_A (IFR) - Spec.pdf'); // bulk-set revision feeds the name expect(r.placed).toBe('m1'); // drop = tracking number only
expect(r.named).toBe('ACM-PRJ-EL-SPC-0001_A (IFR) - Spec.pdf'); // revision from the bulk-set column feeds the name
}); });