feat(classifier): By-transmittal is a per-file grid with a single folder-path input
Standardize the two classify tabs: By-transmittal drops the party→slot→bin tree (and its multi-field "+ Transmittal" form) for a flat per-file grid that mirrors By-tracking. Each file gets ONE editable text input — its full transmittal folder path "<party>/<received|issued>/<YYYY-MM-DD_TN (STATUS) - Title>". Committing it find-or-creates the party/slot/bin; structure stays derived, never stored. Drag-and-drop (from the source tree onto the grid): - plain drop on a routed row → the dropped files JOIN that row's folder; - ⌘/Ctrl-drop on a routed row → a prompt prefilled with that folder's path lets you edit it into a NEW transmittal the files go to (the original is untouched; an unedited path dedups via find-or-create); - drop on empty space / an unrouted row → files are added as blank rows to fill. Model (classify.js): adds a `transmittalWorkset` (parallel to trackingWorkset) plus addToTransmittalGrid / removeFromTransmittalGrid / transmittalGridKeys and setTransmittalPath(keys, path) — the single parser for "<party>/<slot>/<folder>" that also prunes any bin a re-route empties. app.js importPaths now reuses setTransmittalPath for its route axis (one parser, less duplication). Removes the now-dead tree rendering/CRUD (party/bin nodes, binForm, the bin filename editor, the bin drop zone). Tests updated to the grid model: tab render shows the folder-path input; drop join/branch/empty; edit re-routes and prunes the emptied folder; ✕ removes. 71/71 classify specs pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ce6efb0201
commit
9ca24eb3f1
6 changed files with 300 additions and 389 deletions
|
|
@ -751,7 +751,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
|||
}
|
||||
.tg-input:hover { border-color: var(--border); }
|
||||
.tg-input:focus { border-color: var(--primary); background: var(--bg); outline: none; }
|
||||
.tg-tn .tg-input { font-family: var(--mono, monospace); }
|
||||
.tg-tn .tg-input, .tx-path .tg-input { font-family: var(--mono, monospace); }
|
||||
.tg-input.is-warn { border-color: var(--warning, #b8860b); }
|
||||
.tg-orig__link { color: var(--text-muted); white-space: nowrap; text-decoration: none; cursor: pointer; }
|
||||
.tg-orig__link:hover { text-decoration: underline; }
|
||||
|
|
|
|||
|
|
@ -295,29 +295,11 @@
|
|||
note('filename is not a valid ZDDC name "' + filename + '"');
|
||||
}
|
||||
|
||||
// Axis 2 — <party>/<direction>/<transmittal> → transmittal tree (the route).
|
||||
if (leading.length >= 3) {
|
||||
var party = leading[0];
|
||||
var slot = leading[1].toLowerCase();
|
||||
var folder = leading.slice(2).join('/');
|
||||
if (slot !== 'issued' && slot !== 'received') {
|
||||
note('direction must be "issued" or "received", got "' + leading[1] + '"');
|
||||
} else {
|
||||
var pf = window.zddc.parseFolder(folder);
|
||||
if (pf && pf.valid) {
|
||||
var tnParts = pf.trackingNumber.split('-');
|
||||
var seq = tnParts.pop(), type = tnParts.pop();
|
||||
var bid = c.findOrAddTransmittalBin(c.findOrAddParty(party), slot, {
|
||||
date: pf.date, type: type || 'TRN', seq: seq || '', status: pf.status, title: pf.title,
|
||||
});
|
||||
if (bid) { c.place([oldPath], bid, 'transmittal'); didTransmittal = true; }
|
||||
else note('could not create the transmittal folder');
|
||||
} else {
|
||||
note('transmittal folder is not a valid ZDDC folder name "' + folder + '"');
|
||||
}
|
||||
}
|
||||
} else if (leading.length >= 1) {
|
||||
note('to route a transmittal the new path needs <party>/<direction>/<transmittal>/<file>');
|
||||
// Axis 2 — <party>/<direction>/<transmittal> → transmittal tree (the
|
||||
// route). Same parser the By-transmittal grid uses.
|
||||
if (leading.length >= 1) {
|
||||
var terr = c.setTransmittalPath([oldPath], leading.join('/'));
|
||||
if (terr) note(terr); else didTransmittal = true;
|
||||
}
|
||||
|
||||
if (didTracking || didTransmittal) imported++;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
config: defaultConfig(), // tracking-number pattern (fields/statuses/modifiers)
|
||||
worklist: [], // "From a list" scratch rows: [ { id, trackingNumber, title, revisionCell, source, archiveRevisions } ]
|
||||
trackingWorkset: Object.create(null), // srcKeys shown as rows in the By-tracking grid (set: key->true)
|
||||
transmittalWorkset: Object.create(null), // srcKeys shown as rows in the By-transmittal grid (set: key->true)
|
||||
};
|
||||
|
||||
// id -> { node, kind:'tracking'|'party'|'slot'|'transmittal', parent }
|
||||
|
|
@ -422,6 +423,7 @@
|
|||
return { id: r.id, party: r.party, trackingNumber: r.trackingNumber, title: r.title, revisionCell: r.revisionCell, source: r.source, archiveRevisions: r.archiveRevisions };
|
||||
}),
|
||||
trackingWorkset: Object.keys(state.trackingWorkset),
|
||||
transmittalWorkset: Object.keys(state.transmittalWorkset),
|
||||
};
|
||||
}
|
||||
function load(obj) {
|
||||
|
|
@ -434,6 +436,8 @@
|
|||
state.worklist = (Array.isArray(obj.worklist) ? obj.worklist : []).map(normalizeRow);
|
||||
state.trackingWorkset = Object.create(null);
|
||||
(Array.isArray(obj.trackingWorkset) ? obj.trackingWorkset : []).forEach(function (k) { state.trackingWorkset[k] = true; });
|
||||
state.transmittalWorkset = Object.create(null);
|
||||
(Array.isArray(obj.transmittalWorkset) ? obj.transmittalWorkset : []).forEach(function (k) { state.transmittalWorkset[k] = true; });
|
||||
rebuildIndex();
|
||||
migrateLegacyMdl(obj.worklist); // BEFORE anything can prune; materializes old mdl placements
|
||||
notify();
|
||||
|
|
@ -466,6 +470,7 @@
|
|||
state.assignments = {}; state.trackingTree = []; state.transmittalTree = [];
|
||||
state.outputName = null;
|
||||
state.trackingWorkset = Object.create(null);
|
||||
state.transmittalWorkset = Object.create(null);
|
||||
rebuildIndex();
|
||||
notify();
|
||||
}
|
||||
|
|
@ -529,6 +534,84 @@
|
|||
notify();
|
||||
}
|
||||
|
||||
// ── By-transmittal grid (one editable row per file) ──────────────────────
|
||||
// The transmittal tab mirrors the By-tracking grid: a flat, per-file surface
|
||||
// where each file carries ONE text input — its full transmittal folder path
|
||||
// "<party>/<received|issued>/<YYYY-MM-DD_TN (STATUS) - Title>". The path is
|
||||
// PARSED into the transmittal tree (find-or-create party/slot/bin); structure
|
||||
// is still derived, never stored. `transmittalWorkset` keeps a file on the
|
||||
// grid before (and after) it has a path, exactly like `trackingWorkset`.
|
||||
function addToTransmittalGrid(keys) {
|
||||
var changed = false;
|
||||
(keys || []).forEach(function (k) { if (!state.transmittalWorkset[k]) { state.transmittalWorkset[k] = true; changed = true; } });
|
||||
if (changed) notify();
|
||||
}
|
||||
function transmittalGridKeys() {
|
||||
var set = Object.create(null);
|
||||
Object.keys(state.transmittalWorkset).forEach(function (k) { set[k] = true; });
|
||||
Object.keys(state.assignments).forEach(function (k) { if (state.assignments[k].transmittalNodeId) set[k] = true; });
|
||||
return Object.keys(set);
|
||||
}
|
||||
function transmittalHasFiles(binId) {
|
||||
for (var k in state.assignments) { if (state.assignments[k].transmittalNodeId === binId) return true; }
|
||||
return false;
|
||||
}
|
||||
// Delete a transmittal bin once nothing points at it (so re-routing doesn't
|
||||
// litter the tree); drop the party too if it has no remaining bins.
|
||||
function pruneEmptyTransmittal(binId) {
|
||||
var info = infoFor(binId);
|
||||
if (!info || info.kind !== 'transmittal' || transmittalHasFiles(binId)) return;
|
||||
var slotInfo = info.parent ? infoFor(info.parent.id) : null;
|
||||
var party = slotInfo && slotInfo.parent ? slotInfo.parent : null;
|
||||
deleteNode(binId); // rebuilds the index + clears danglers
|
||||
if (party) {
|
||||
var anyBin = (party.children || []).some(function (slot) { return (slot.children || []).length; });
|
||||
if (!anyBin) deleteNode(party.id);
|
||||
}
|
||||
}
|
||||
function removeFromTransmittalGrid(key) {
|
||||
var a = state.assignments[key], old = a ? a.transmittalNodeId : null;
|
||||
delete state.transmittalWorkset[key];
|
||||
place([key], null, 'transmittal');
|
||||
if (old) pruneEmptyTransmittal(old);
|
||||
notify();
|
||||
}
|
||||
// Route keys to the transmittal named by a "<party>/<slot>/<folder>" path,
|
||||
// creating party/slot/bin as needed. Blank path clears the placement (the row
|
||||
// stays, unrouted). Returns '' on success or a short error message; on error
|
||||
// nothing is changed. Empties out (and prunes) any bin a key leaves behind.
|
||||
function setTransmittalPath(keys, path) {
|
||||
keys = keys || [];
|
||||
path = (path == null ? '' : String(path)).trim();
|
||||
var oldBins = Object.create(null);
|
||||
keys.forEach(function (k) { var a = state.assignments[k]; if (a && a.transmittalNodeId) oldBins[a.transmittalNodeId] = true; });
|
||||
if (!path) {
|
||||
place(keys, null, 'transmittal');
|
||||
keys.forEach(function (k) { state.transmittalWorkset[k] = true; });
|
||||
Object.keys(oldBins).forEach(pruneEmptyTransmittal);
|
||||
notify();
|
||||
return '';
|
||||
}
|
||||
var segs = path.split('/').filter(function (s) { return s !== ''; });
|
||||
if (segs.length < 3) return 'path must be <party>/<direction>/<transmittal>';
|
||||
var party = segs[0], slot = segs[1].toLowerCase(), folder = segs.slice(2).join('/');
|
||||
if (slot !== 'issued' && slot !== 'received') return 'direction must be "issued" or "received"';
|
||||
var pf = zddc.parseFolder(folder);
|
||||
if (!pf || !pf.valid) return 'not a valid transmittal folder "YYYY-MM-DD_TN (STATUS) - Title"';
|
||||
var tnParts = pf.trackingNumber.split('-');
|
||||
var seq = tnParts.pop(), type = tnParts.pop();
|
||||
var bid = findOrAddTransmittalBin(findOrAddParty(party), slot, {
|
||||
date: pf.date, type: type || 'TRN', seq: seq || '', status: pf.status, title: pf.title,
|
||||
});
|
||||
if (!bid) return 'could not create the transmittal';
|
||||
place(keys, bid, 'transmittal');
|
||||
keys.forEach(function (k) { state.transmittalWorkset[k] = true; });
|
||||
delete oldBins[bid]; // keep the bin we just filled
|
||||
Object.keys(oldBins).forEach(pruneEmptyTransmittal);
|
||||
notify();
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── pattern config ───────────────────────────────────────────────────────
|
||||
function normalizeConfig(c) {
|
||||
var d = defaultConfig();
|
||||
|
|
@ -983,6 +1066,9 @@
|
|||
// By-tracking grid
|
||||
addToTrackingGrid: addToTrackingGrid, removeFromTrackingGrid: removeFromTrackingGrid,
|
||||
trackingGridKeys: trackingGridKeys, setFileIdentity: setFileIdentity, forgetFile: forgetFile,
|
||||
// By-transmittal grid
|
||||
addToTransmittalGrid: addToTransmittalGrid, removeFromTransmittalGrid: removeFromTransmittalGrid,
|
||||
transmittalGridKeys: transmittalGridKeys, setTransmittalPath: setTransmittalPath,
|
||||
setWorklist: setWorklist, appendWorklist: appendWorklist, clearWorklist: clearWorklist,
|
||||
removeWorklistRow: removeWorklistRow,
|
||||
getWorklist: getWorklist, getWorklistRow: getWorklistRow,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
/**
|
||||
* 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": <party>/{received,issued}/<transmittal folder>.
|
||||
* Two orthogonal per-file grids (both on the shared seltable) the user maps
|
||||
* files onto — one editable row per file:
|
||||
* - "By tracking number": Tracking# / Rev (Status) / Title cells compose the
|
||||
* ZDDC filename (the rename).
|
||||
* - "By transmittal": one text input = the file's full transmittal folder path
|
||||
* "<party>/<received|issued>/<YYYY-MM-DD_TN (STATUS) - Title>" (the route);
|
||||
* committing it find-or-creates the party/slot/bin in classify.js.
|
||||
*
|
||||
* 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.
|
||||
* Structure + placements live in classify.js; everything shown here is derived,
|
||||
* never stored. Drops are handled per-grid (setupGridDrop / setupTransmittalDrop)
|
||||
* so a ⌘/Ctrl transmittal drop can branch a new folder.
|
||||
*/
|
||||
(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;
|
||||
var currentTab = 'tracking'; // 'tracking' | 'transmittal' — active tab
|
||||
var listScanned = false; // a Load has run this session (drives the "new" badge)
|
||||
|
|
@ -39,7 +38,6 @@
|
|||
addFilteredBtn: document.getElementById('addFilteredBtn'),
|
||||
renameBtn: document.getElementById('renameBtn'),
|
||||
trackingColsBtn: document.getElementById('trackingColsBtn'),
|
||||
addPartyBtn: document.getElementById('addPartyBtn'),
|
||||
stats: document.getElementById('classifyStats'),
|
||||
};
|
||||
|
||||
|
|
@ -70,16 +68,9 @@
|
|||
if (text) { e.preventDefault(); openPasteDialog(text); }
|
||||
});
|
||||
if (els.trackingColsBtn) els.trackingColsBtn.addEventListener('click', openColumnChooser);
|
||||
els.addPartyBtn.addEventListener('click', function () {
|
||||
var name = prompt('Party name (also the transmittal-number prefix):', '');
|
||||
if (name && name.trim()) C().addParty(name.trim());
|
||||
});
|
||||
|
||||
els.transmittalTree.addEventListener('click', onTransmittalClick);
|
||||
els.transmittalTree.addEventListener('change', onFileNameChange);
|
||||
|
||||
setupGridDrop(els.trackingTree);
|
||||
setupDropZone(els.transmittalTree, 'transmittal');
|
||||
setupTransmittalDrop(els.transmittalTree);
|
||||
|
||||
C().on(render);
|
||||
if (window.app.modules.store && window.app.modules.store.on) {
|
||||
|
|
@ -101,24 +92,6 @@
|
|||
})(window.app.folderTree || []);
|
||||
return out;
|
||||
}
|
||||
// One pass: group files by the node they're placed in, per axis.
|
||||
function buildPlaced(files) {
|
||||
var c = C(), byT = {}, byX = {}, byTn = {};
|
||||
files.forEach(function (f) {
|
||||
var a = c.getAssignment(c.srcKeyForFile(f));
|
||||
if (!a) return;
|
||||
if (a.trackingNodeId) {
|
||||
(byT[a.trackingNodeId] = byT[a.trackingNodeId] || []).push(f);
|
||||
// Also index by tracking NUMBER so a "From a list" row can show
|
||||
// the files placed under it (a row is a tracking number, not a node).
|
||||
var tn = c.deriveTarget(f).tracking;
|
||||
if (tn) (byTn[tn] = byTn[tn] || []).push(f);
|
||||
}
|
||||
if (a.transmittalNodeId) (byX[a.transmittalNodeId] = byX[a.transmittalNodeId] || []).push(f);
|
||||
});
|
||||
return { tracking: byT, transmittal: byX, byTracking: byTn };
|
||||
}
|
||||
|
||||
function showTab(which) {
|
||||
currentTab = (which === 'transmittal') ? 'transmittal' : 'tracking';
|
||||
els.trackingTab.classList.toggle('active', currentTab === 'tracking');
|
||||
|
|
@ -138,9 +111,8 @@
|
|||
function render() {
|
||||
if (!initialized || !C().isEnabled()) return;
|
||||
var files = allFiles();
|
||||
var placed = buildPlaced(files);
|
||||
renderTrackingGrid(els.trackingTree);
|
||||
renderTransmittalInto(els.transmittalTree, C().getTransmittalTree(), placed.transmittal);
|
||||
renderTransmittalGrid(els.transmittalTree);
|
||||
renderStats(files);
|
||||
}
|
||||
|
||||
|
|
@ -184,66 +156,13 @@
|
|||
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;
|
||||
}
|
||||
|
||||
// Placed files inside a transmittal bin. Each row is draggable (drag onto
|
||||
// another bin to MOVE it) and carries an ✕ to remove it from the transmittal.
|
||||
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.dataset.key = d.key;
|
||||
row.draggable = true;
|
||||
row.addEventListener('dragstart', function (e) { window.app.modules.dnd.setDrag([d.key], e); });
|
||||
var orig = el('span', 'tfile__orig', f.originalFilename + (f.extension ? '.' + f.extension : ''));
|
||||
orig.title = 'Drag to another transmittal to move · click to preview';
|
||||
row.appendChild(orig);
|
||||
row.appendChild(el('span', 'tfile__arrow', '→'));
|
||||
// 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);
|
||||
var rm = el('button', 'tnode__act tfile__remove', '✕');
|
||||
rm.dataset.act = 'untransmit';
|
||||
rm.title = 'Remove from this transmittal';
|
||||
row.appendChild(rm);
|
||||
box.appendChild(row);
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
// ── name filter (the autofilter box above the target trees) ────────────
|
||||
// ── name filter (the autofilter box above the target grids) ────────────
|
||||
// Mirrored into each grid's own global filter (seltable.setFilter) on render.
|
||||
var rfTerms = [];
|
||||
function setNameFilter(q) {
|
||||
rfTerms = String(q || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||
render();
|
||||
}
|
||||
function rfActive() { return rfTerms.length > 0; }
|
||||
function rfHit(text) {
|
||||
if (!rfTerms.length) return true;
|
||||
var t = String(text || '').toLowerCase();
|
||||
for (var i = 0; i < rfTerms.length; i++) { if (t.indexOf(rfTerms[i]) === -1) return false; }
|
||||
return true;
|
||||
}
|
||||
// A placed-file row matches on its original name or its derived ZDDC name.
|
||||
function fileRowMatches(f) {
|
||||
var orig = f.originalFilename + (f.extension ? '.' + f.extension : '');
|
||||
return rfHit(orig) || rfHit(C().deriveTarget(f).filename || '');
|
||||
}
|
||||
|
||||
// ── By-tracking: flat editable grid (one row per file), on the shared
|
||||
// seltable — so it gets multi-sort + per-column autofilters + resizable,
|
||||
// persisted widths for free. Only `hidden` (the Columns ▾ chooser) is
|
||||
|
|
@ -531,93 +450,85 @@
|
|||
}, 0);
|
||||
}
|
||||
|
||||
// Transmittal tree
|
||||
function renderTransmittalInto(container, parties, placedMap) {
|
||||
container.textContent = '';
|
||||
if (!parties.length) {
|
||||
container.appendChild(el('div', 'target-empty', 'No parties yet — “+ Party” to start.'));
|
||||
// ── By-transmittal: flat editable grid (one row per file), mirroring the
|
||||
// By-tracking grid. Each row's single text input is the file's full
|
||||
// transmittal folder path "<party>/<received|issued>/<folder>"; committing
|
||||
// it routes the file (classify.setTransmittalPath find-or-creates the
|
||||
// party/slot/bin). Drops are handled at the container level so a ⌘/Ctrl
|
||||
// drop can branch a new transmittal (setupTransmittalDrop). ──────────────
|
||||
function txPath(f) { return C().deriveTarget(f).outPath || ''; }
|
||||
function txStatusCell(td, f) {
|
||||
var ok = !!txPath(f);
|
||||
var badge = el('span', ok ? 'tfile__badge tfile__badge--ok' : 'tfile__badge tg-wanted', ok ? '✓' : '◇');
|
||||
badge.title = ok ? ('Routed to ' + txPath(f)) : 'No transmittal folder yet — type one, or drop onto a routed row.';
|
||||
td.appendChild(badge);
|
||||
}
|
||||
function txPathCell(td, f) {
|
||||
var c = C(), key = c.srcKeyForFile(f);
|
||||
editCell(td, 'tg-input', txPath(f), 'Acme/received/2026-06-18_Acme-TRN-0001 (IFC) - Title', function (v) {
|
||||
var err = c.setTransmittalPath([key], v);
|
||||
if (err) { window.zddc.toast('Transmittal not set — ' + err, 'warning'); render(); }
|
||||
});
|
||||
}
|
||||
function txRemoveCell(td, f) {
|
||||
var c = C(), key = c.srcKeyForFile(f);
|
||||
var rm = el('button', 'tnode__act tg-x__btn', '✕'); rm.title = 'Remove from the transmittal grid';
|
||||
rm.addEventListener('click', function () { c.removeFromTransmittalGrid(key); });
|
||||
td.appendChild(rm);
|
||||
}
|
||||
function transmittalGridRows() {
|
||||
var out = [];
|
||||
C().transmittalGridKeys().forEach(function (k) {
|
||||
var f = fileByKey(k); if (f) out.push({ kind: 'file', file: f, id: 'f:' + k });
|
||||
});
|
||||
return out;
|
||||
}
|
||||
function transmittalColumns() {
|
||||
return [
|
||||
{ key: 'status', title: 'Status', cls: 'tg-status', filterable: false,
|
||||
get: function (r) { return txPath(r.file) ? 'ok' : 'awaiting'; },
|
||||
render: function (r, td) { txStatusCell(td, r.file); } },
|
||||
{ key: 'orig', title: 'Original name', cls: 'tg-orig',
|
||||
get: function (r) { return joinName(r.file); },
|
||||
render: function (r, td) { gridOrigCell(td, r.file); } },
|
||||
{ key: 'path', title: 'Transmittal folder', cls: 'tx-path',
|
||||
get: function (r) { return txPath(r.file); },
|
||||
render: function (r, td) { txPathCell(td, r.file); } },
|
||||
{ key: 'x', title: '', cls: 'tg-x', sortable: false, filterable: false,
|
||||
render: function (r, td) { txRemoveCell(td, r.file); } },
|
||||
];
|
||||
}
|
||||
var transmittalGrid = null;
|
||||
function ensureTransmittalGrid(container) {
|
||||
if (transmittalGrid) return transmittalGrid;
|
||||
transmittalGrid = window.app.modules.seltable.create({
|
||||
container: container,
|
||||
rows: transmittalGridRows,
|
||||
rowId: function (r) { return r.id; },
|
||||
columns: transmittalColumns(),
|
||||
persistKey: 'zddc.classifier.transmittalCols',
|
||||
});
|
||||
transmittalGrid.render();
|
||||
return transmittalGrid;
|
||||
}
|
||||
function renderTransmittalGrid(container) {
|
||||
if (!transmittalGridRows().length) {
|
||||
transmittalGrid = null;
|
||||
container.textContent = '';
|
||||
container.classList.remove('seltable');
|
||||
container.appendChild(el('div', 'target-empty',
|
||||
'No files here yet — drag files in, then type each one’s transmittal folder '
|
||||
+ '(<party>/<received|issued>/<YYYY-MM-DD_TN (STATUS) - Title>). Drop onto a routed '
|
||||
+ 'row to put the file in that same folder; ⌘/Ctrl-drop to branch a new transmittal from it.'));
|
||||
return;
|
||||
}
|
||||
parties.forEach(function (p) { var e = partyNode(p, placedMap); if (e) container.appendChild(e); });
|
||||
if (rfActive() && !container.children.length) {
|
||||
container.appendChild(el('div', 'target-empty', 'No matches in the transmittal tree.'));
|
||||
}
|
||||
}
|
||||
function partyNode(party, placedMap) {
|
||||
var partyMatch = rfHit(party.name);
|
||||
var slotEls = [], anyBin = false;
|
||||
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) {
|
||||
var be = binNode(bin, placedMap, partyMatch);
|
||||
if (be) { sw.appendChild(be); anyBin = true; }
|
||||
});
|
||||
slotEls.push(sw);
|
||||
});
|
||||
if (rfActive() && !partyMatch && !anyBin) return null;
|
||||
|
||||
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);
|
||||
slotEls.forEach(function (sw) { wrap.appendChild(sw); });
|
||||
return wrap;
|
||||
}
|
||||
function binNode(bin, placedMap, ancMatched) {
|
||||
var matched = ancMatched || rfHit(bin.name || '');
|
||||
var placed = placedMap[bin.id] || [];
|
||||
var shownFiles = (rfActive() && !matched) ? placed.filter(fileRowMatches) : placed;
|
||||
if (rfActive() && !matched && !shownFiles.length) return null;
|
||||
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)'));
|
||||
if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length)));
|
||||
row.appendChild(nodeActions([
|
||||
{ act: 'rename-bin', label: '✎', title: 'Rename transmittal' },
|
||||
{ act: 'del', label: '🗑', title: 'Delete transmittal' },
|
||||
]));
|
||||
wrap.appendChild(row);
|
||||
if (shownFiles.length) wrap.appendChild(fileList(shownFiles));
|
||||
return wrap;
|
||||
ensureTransmittalGrid(container);
|
||||
transmittalGrid.setFilter(rfTerms.join(' '));
|
||||
}
|
||||
|
||||
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;
|
||||
function setGridStatus(text) {
|
||||
var s = document.getElementById('scanStatus');
|
||||
if (s) { s.textContent = text; s.classList.toggle('scanning', !!text); }
|
||||
}
|
||||
|
||||
function setGridStatus(text) {
|
||||
|
|
@ -937,163 +848,75 @@
|
|||
}
|
||||
|
||||
// ── events ─────────────────────────────────────────────────────────────
|
||||
function closestNodeId(target) {
|
||||
var n = target.closest('[data-id]');
|
||||
return n ? n.dataset.id : null;
|
||||
}
|
||||
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) {
|
||||
// Preview link on a revision cell (its placed file).
|
||||
var pl = e.target.closest('[data-preview-key]');
|
||||
if (pl) {
|
||||
e.preventDefault();
|
||||
var pf = fileByKey(pl.dataset.previewKey);
|
||||
if (pf && window.app.modules.preview && window.app.modules.preview.previewFile) {
|
||||
window.app.modules.preview.previewFile(pf);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e.target.closest('[data-act]')) return false; // action button — not a preview
|
||||
if (e.target.closest('.tfile__name')) return false;
|
||||
var tf = e.target.closest('.tfile');
|
||||
if (!tf || !tf.dataset.key) return false;
|
||||
var f = fileByKey(tf.dataset.key);
|
||||
if (f && window.app.modules.preview && window.app.modules.preview.previewFile) {
|
||||
window.app.modules.preview.previewFile(f);
|
||||
}
|
||||
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.
|
||||
}
|
||||
function onTransmittalClick(e) {
|
||||
if (previewFromTarget(e)) return;
|
||||
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;
|
||||
// ── By-transmittal drops ─────────────────────────────────────────────────
|
||||
// Handled at the container level (not seltable's per-row onRowDrop) so the
|
||||
// drop event's modifier key is available:
|
||||
// plain drop on a routed row → the dropped files JOIN that row's folder.
|
||||
// ⌘/Ctrl drop on a routed row → prompt, prefilled with that folder's path,
|
||||
// so the user edits it into a NEW transmittal the files go to (the
|
||||
// original folder is untouched — find-or-create dedups an unedited path).
|
||||
// drop on empty space / an unrouted row → just add the files as grid rows.
|
||||
function setupTransmittalDrop(container) {
|
||||
function rowUnder(e) { var tr = e.target.closest && e.target.closest('.seltable__row'); return (tr && container.contains(tr)) ? tr : null; }
|
||||
function clearHover() {
|
||||
Array.prototype.forEach.call(container.querySelectorAll('.drop-hover'), function (n) { n.classList.remove('drop-hover'); });
|
||||
container.classList.remove('tg-drop-hover');
|
||||
}
|
||||
if (act === 'untransmit') {
|
||||
var tf = btn.closest('.tfile');
|
||||
if (tf && tf.dataset.key) C().place([tf.dataset.key], null, 'transmittal');
|
||||
return;
|
||||
}
|
||||
if (act === 'rename-bin') {
|
||||
var bid = closestNodeId(btn);
|
||||
var bn = C().getNode(bid);
|
||||
var nn = prompt('Rename transmittal (this becomes its folder name):', bn ? bn.name : '');
|
||||
if (nn && nn.trim()) C().renameNode(bid, nn.trim());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ── drop targets ───────────────────────────────────────────────────────
|
||||
// Resolve the drop target under an event:
|
||||
// tracking → any folder node (.tnode)
|
||||
// transmittal → a transmittal bin only (.tnode--bin)
|
||||
function dropTarget(target, axis) {
|
||||
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');
|
||||
for (var i = 0; i < hot.length; i++) hot[i].classList.remove('drop-hover');
|
||||
}
|
||||
function setupDropZone(container, axis) {
|
||||
container.addEventListener('dragover', function (e) {
|
||||
if (!window.app.modules.dnd.active()) return;
|
||||
var t = dropTarget(e.target, axis);
|
||||
clearHover(container);
|
||||
if (!t) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
t.row.classList.add('drop-hover');
|
||||
});
|
||||
container.addEventListener('dragleave', function (e) {
|
||||
if (e.target === container) clearHover(container);
|
||||
e.preventDefault(); e.dataTransfer.dropEffect = 'copy';
|
||||
clearHover();
|
||||
var tr = rowUnder(e);
|
||||
if (tr) tr.classList.add('drop-hover'); else container.classList.add('tg-drop-hover');
|
||||
});
|
||||
container.addEventListener('dragleave', function (e) { if (e.target === container) clearHover(); });
|
||||
container.addEventListener('drop', function (e) {
|
||||
var t = dropTarget(e.target, axis);
|
||||
clearHover(container);
|
||||
if (!t) return;
|
||||
e.preventDefault();
|
||||
var tr = rowUnder(e), meta = e.metaKey || e.ctrlKey;
|
||||
clearHover();
|
||||
var keys = window.app.modules.dnd.getDrag();
|
||||
window.app.modules.dnd.clearDrag();
|
||||
if (!keys.length) return;
|
||||
C().place(keys, t.id, axis);
|
||||
onTransmittalDrop(keys, tr ? tr.dataset.id : null, meta);
|
||||
});
|
||||
}
|
||||
function onTransmittalDrop(keys, rowId, meta) {
|
||||
var c = C(), targetPath = '';
|
||||
if (rowId && rowId.indexOf('f:') === 0) {
|
||||
var tf = fileByKey(rowId.slice(2));
|
||||
if (tf) targetPath = c.deriveTarget(tf).outPath || '';
|
||||
}
|
||||
if (targetPath) {
|
||||
if (meta) {
|
||||
var edited = prompt('New transmittal folder — edit to branch a copy, or keep to join:', targetPath);
|
||||
if (edited == null) return; // cancelled
|
||||
var err = c.setTransmittalPath(keys, edited.trim());
|
||||
if (err) window.zddc.toast('Could not route — ' + err, 'warning');
|
||||
} else {
|
||||
c.setTransmittalPath(keys, targetPath); // join the same folder
|
||||
}
|
||||
return;
|
||||
}
|
||||
c.addToTransmittalGrid(keys); // empty space / unrouted row → add blank rows to fill
|
||||
}
|
||||
|
||||
// Reveal a source key's placement in the target pane (source → target).
|
||||
function reveal(key) {
|
||||
var a = C().getAssignment(key);
|
||||
if (!a) return;
|
||||
// Both tabs are per-file grids whose rows are keyed "f:<srcKey>".
|
||||
if (a.trackingNodeId) {
|
||||
showTab('tracking'); collapsed = {}; render();
|
||||
flashNode(els.trackingTree, a.trackingNodeId);
|
||||
showTab('tracking'); render();
|
||||
flashNode(els.trackingTree, 'f:' + key);
|
||||
} else if (a.transmittalNodeId) {
|
||||
showTab('transmittal'); render();
|
||||
flashNode(els.transmittalTree, a.transmittalNodeId);
|
||||
flashNode(els.transmittalTree, 'f:' + key);
|
||||
}
|
||||
}
|
||||
function flashNode(container, id) {
|
||||
|
|
|
|||
|
|
@ -130,13 +130,12 @@
|
|||
</section>
|
||||
<section id="transmittalPanel" class="target-panel" hidden>
|
||||
<div class="target-panel__toolbar">
|
||||
<button id="addPartyBtn" class="btn btn-sm btn-secondary">+ Party</button>
|
||||
<span class="target-hint"><party>/{received,issued}/<transmittal>. Drag files (or a whole folder) into a transmittal, then Copy into the archive.</span>
|
||||
<span class="target-hint">One row per file — type its transmittal folder: <party>/<received|issued>/<YYYY-MM-DD_TN (STATUS) - Title>. Drag files in: drop on a row to put the file in that same folder, ⌘/Ctrl-drop to branch a new transmittal from it.</span>
|
||||
<button id="checkDuplicatesBtn" class="btn btn-secondary btn-sm" title="Check for files with the same tracking number + revision but different content (flagged ≠ in red)">Check</button>
|
||||
<button id="copyOutputBtn" class="btn btn-primary btn-sm" disabled title="Copy fully-classified files (+ their transmittal folders) into the archive — server or a local folder. Source untouched, resumable, verified.">Copy…</button>
|
||||
</div>
|
||||
<input type="search" id="transmittalFilterInput" class="tree-filter target-filter" spellcheck="false"
|
||||
placeholder="Filter the transmittal tree…" aria-label="Filter transmittal tree">
|
||||
placeholder="Filter the transmittal grid…" aria-label="Filter transmittal grid">
|
||||
<div id="transmittalTree" class="target-tree"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -164,16 +164,18 @@ test('target tree renders the By-tracking grid and tabs switch', async ({ page }
|
|||
window.app.folderTree = [{ name: 'R', path: 'R', files: [f], children: [] }];
|
||||
c.setFileIdentity(c.srcKeyForFile(f), { tracking: 'ACME-PROJ-EL-DWG-0001', rev: 'A (IFR)', title: 'Spec' });
|
||||
const party = c.addParty('ClientCorp');
|
||||
c.addTransmittalBin(party, 'received', { date: '2026-03-14', type: 'TRN', seq: '0007' });
|
||||
const bin = c.addTransmittalBin(party, 'received', { date: '2026-03-14', type: 'TRN', seq: '0007' });
|
||||
c.place([c.srcKeyForFile(f)], bin, 'transmittal'); // route it so it's a transmittal-grid row
|
||||
window.app.modules.targetTree.render();
|
||||
});
|
||||
// The grid (now on the shared seltable) shows the tracking number in a cell.
|
||||
await expect(page.locator('#trackingTree .seltable__table')).toBeVisible();
|
||||
await expect(page.locator('#trackingTree .tg-tn .tg-input')).toHaveValue('ACME-PROJ-EL-DWG-0001');
|
||||
// Switch to transmittal tab.
|
||||
// Switch to transmittal tab — also a per-file grid, one folder-path input per file.
|
||||
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();
|
||||
await expect(page.locator('#transmittalTree .tx-path .tg-input'))
|
||||
.toHaveValue('ClientCorp/received/2026-03-14_ClientCorp-TRN-0007 (---) - Transmittal');
|
||||
});
|
||||
|
||||
// ── Phase 3: drag-and-drop assignment (drop handler) ───────────────────────
|
||||
|
|
@ -202,33 +204,52 @@ test('dropping files onto the By-tracking grid adds rows and auto-fills ZDDC nam
|
|||
expect(r.plainTn).toBe(''); // the plain file is a blank row to fill in
|
||||
});
|
||||
|
||||
test('dropping onto a transmittal bin assigns; dropping on a party row does not', async ({ page }) => {
|
||||
test('transmittal grid: plain drop on a row joins its folder, ⌘-drop branches, empty drop adds a row', async ({ page }) => {
|
||||
await page.evaluate(() => window.app.modules.app.setMode());
|
||||
await page.click('#transmittalTab');
|
||||
const r = await page.evaluate(() => {
|
||||
const c = window.app.modules.classify;
|
||||
const party = c.addParty('ClientCorp');
|
||||
const bin = c.addTransmittalBin(party, 'received', { date: '2026-03-14', type: 'TRN', seq: '0007' });
|
||||
window.app.modules.targetTree.render();
|
||||
const key = 'Sub/foundation.pdf';
|
||||
const c = window.app.modules.classify, tt = window.app.modules.targetTree;
|
||||
c.reset();
|
||||
const fA = { originalFilename: 'A', extension: 'pdf', folderPath: 'R' };
|
||||
window.app.folderTree = [{ name: 'R', path: 'R', files: [fA], children: [] }];
|
||||
const keyA = c.srcKeyForFile(fA);
|
||||
// Route file A into a folder so its row is a drop target.
|
||||
c.setTransmittalPath([keyA], 'ClientCorp/received/2026-03-14_ClientCorp-TRN-0007 (IFC) - Pkg');
|
||||
tt.render();
|
||||
const rowA = document.querySelector('#transmittalTree .seltable__row');
|
||||
const binA = c.getAssignment(keyA).transmittalNodeId;
|
||||
|
||||
// Drop on the bin → assigned.
|
||||
const binRow = document.querySelector('#transmittalTree .tnode--bin .tnode__row');
|
||||
window.app.modules.dnd.setDrag([key]);
|
||||
binRow.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true }));
|
||||
const afterBin = c.assignmentFor(key).transmittalNodeId;
|
||||
// Plain drop of B onto A's row → B JOINS the same folder.
|
||||
const keyB = 'R/B.pdf';
|
||||
window.app.modules.dnd.setDrag([keyB]);
|
||||
rowA.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true }));
|
||||
const bJoined = c.getAssignment(keyB).transmittalNodeId;
|
||||
|
||||
// Reset, then drop on the party row → ignored (only bins are targets).
|
||||
c.place([key], null, 'transmittal');
|
||||
const partyRow = document.querySelector('#transmittalTree .tnode--party > .tnode__row');
|
||||
window.app.modules.dnd.setDrag([key]);
|
||||
partyRow.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true }));
|
||||
const afterParty = c.assignmentFor(key).transmittalNodeId;
|
||||
// ⌘-drop of D onto A's row → a NEW folder (prompt supplies the edited name).
|
||||
window.prompt = function () { return 'ClientCorp/received/2026-03-15_ClientCorp-TRN-0008 (IFC) - New'; };
|
||||
const keyD = 'R/D.pdf';
|
||||
window.app.modules.dnd.setDrag([keyD]);
|
||||
const metaEv = new Event('drop', { bubbles: true, cancelable: true }); metaEv.metaKey = true;
|
||||
rowA.dispatchEvent(metaEv);
|
||||
const dBin = c.getAssignment(keyD).transmittalNodeId;
|
||||
|
||||
return { afterBin, bin, afterParty };
|
||||
// Drop of C on empty grid space → added as a row, not routed.
|
||||
const keyC = 'R/C.pdf';
|
||||
window.app.modules.dnd.setDrag([keyC]);
|
||||
document.querySelector('#transmittalTree').dispatchEvent(new Event('drop', { bubbles: true, cancelable: true }));
|
||||
const cAssign = c.getAssignment(keyC);
|
||||
|
||||
return {
|
||||
binA, bJoined, dBin,
|
||||
cInGrid: c.transmittalGridKeys().indexOf(keyC) !== -1,
|
||||
cRouted: !!(cAssign && cAssign.transmittalNodeId),
|
||||
};
|
||||
});
|
||||
expect(r.afterBin).toBe(r.bin);
|
||||
expect(r.afterParty).toBe(null);
|
||||
expect(r.bJoined).toBe(r.binA); // plain drop joined A's folder
|
||||
expect(r.dBin).not.toBe(r.binA); // ⌘-drop branched a different folder
|
||||
expect(r.dBin).toBeTruthy();
|
||||
expect(r.cInGrid).toBe(true); // empty drop added C as a grid row
|
||||
expect(r.cRouted).toBe(false); // …but left it unrouted
|
||||
});
|
||||
|
||||
// ── Phase 4: left-tree markers, exclude, cross-tree find ───────────────────
|
||||
|
|
@ -1204,7 +1225,7 @@ test('copy: verifies copied bytes; a bad write fails verification and is removed
|
|||
expect(r.left).toBe(0); // …so a re-run re-copies it
|
||||
});
|
||||
|
||||
test('transmittal: rename a bin (feeds the folder), remove and move a placed file', async ({ page }) => {
|
||||
test('transmittal grid: editing the path re-routes (branching folders); ✕ removes the file', async ({ page }) => {
|
||||
await page.evaluate(() => window.app.modules.app.setMode());
|
||||
const r = await page.evaluate(async () => {
|
||||
const c = window.app.modules.classify, tt = window.app.modules.targetTree;
|
||||
|
|
@ -1212,40 +1233,40 @@ test('transmittal: rename a bin (feeds the folder), remove and move a placed fil
|
|||
const f = { originalFilename: 'doc', extension: 'pdf', folderPath: 'R' };
|
||||
window.app.folderTree = [{ name: 'R', path: 'R', files: [f], children: [] }];
|
||||
const key = c.srcKeyForFile(f);
|
||||
const party = c.addParty('CC');
|
||||
const bin1 = c.addTransmittalBin(party, 'received', { date: '2026-03-14', type: 'TRN', seq: '0007' });
|
||||
const bin2 = c.addTransmittalBin(party, 'received', { date: '2026-03-14', type: 'TRN', seq: '0008' });
|
||||
c.place([key], bin1, 'transmittal');
|
||||
const p1 = 'CC/received/2026-03-14_CC-TRN-0007 (IFC) - First';
|
||||
const p2 = 'CC/received/2026-03-14_CC-TRN-0008 (IFC) - Second';
|
||||
const err = c.setTransmittalPath([key], p1);
|
||||
tt.showTab('transmittal'); tt.render();
|
||||
const out1 = c.deriveTarget(f).outPath;
|
||||
const bin1 = c.getAssignment(key).transmittalNodeId;
|
||||
|
||||
// Rename the bin → it becomes the copy folder name.
|
||||
c.renameNode(bin1, 'My Custom Transmittal');
|
||||
const renamed = c.getNode(bin1).name === 'My Custom Transmittal';
|
||||
const folder = c.deriveTarget(f).transmittalFolder;
|
||||
// Edit the single folder-path input → re-routes to a new folder.
|
||||
const input = document.querySelector('#transmittalTree .tx-path .tg-input');
|
||||
const prefilled = input.value;
|
||||
input.value = p2; input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
const out2 = c.deriveTarget(f).outPath;
|
||||
const rerouted = c.getAssignment(key).transmittalNodeId !== bin1;
|
||||
// The now-empty first folder is pruned (re-routing doesn't litter the tree).
|
||||
const oldPruned = !c.getNode(bin1);
|
||||
|
||||
// The placed-file row is draggable (move) and carries a remove button.
|
||||
// ✕ removes the file from the transmittal grid entirely.
|
||||
tt.render();
|
||||
const row = document.querySelector('#transmittalTree .tfile[data-key]');
|
||||
const draggable = !!(row && row.draggable);
|
||||
const hasRemove = !!(row && row.querySelector('.tfile__remove[data-act="untransmit"]'));
|
||||
|
||||
// Remove from the transmittal (click ✕).
|
||||
row.querySelector('.tfile__remove').click();
|
||||
const a1 = c.getAssignment(key);
|
||||
const removed = !(a1 && a1.transmittalNodeId);
|
||||
|
||||
// Move = re-place onto another bin (what dropping on bin2 does).
|
||||
c.place([key], bin2, 'transmittal');
|
||||
const movedTo = (c.getAssignment(key) || {}).transmittalNodeId === bin2;
|
||||
|
||||
return { renamed, folder, draggable, hasRemove, removed, movedTo };
|
||||
document.querySelector('#transmittalTree .tg-x .tg-x__btn').click();
|
||||
const a = c.getAssignment(key);
|
||||
return {
|
||||
err, prefilled, out1, out2, rerouted, oldPruned,
|
||||
removed: !(a && a.transmittalNodeId),
|
||||
gone: c.transmittalGridKeys().indexOf(key) === -1,
|
||||
};
|
||||
});
|
||||
expect(r.renamed).toBe(true);
|
||||
expect(r.folder).toBe('My Custom Transmittal'); // rename drives the filing folder
|
||||
expect(r.draggable).toBe(true);
|
||||
expect(r.hasRemove).toBe(true);
|
||||
expect(r.err).toBe('');
|
||||
expect(r.prefilled).toBe('CC/received/2026-03-14_CC-TRN-0007 (IFC) - First'); // input shows the current folder
|
||||
expect(r.out1).toBe('CC/received/2026-03-14_CC-TRN-0007 (IFC) - First');
|
||||
expect(r.out2).toBe('CC/received/2026-03-14_CC-TRN-0008 (IFC) - Second'); // edit re-routed
|
||||
expect(r.rerouted).toBe(true);
|
||||
expect(r.oldPruned).toBe(true);
|
||||
expect(r.removed).toBe(true);
|
||||
expect(r.movedTo).toBe(true);
|
||||
expect(r.gone).toBe(true);
|
||||
});
|
||||
|
||||
test('seltable: autofilter + ctrl-shift selection builds complex sets', async ({ page }) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue