chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 23s
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 23s
This commit is contained in:
parent
9ca24eb3f1
commit
8fda925992
7 changed files with 359 additions and 413 deletions
|
|
@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Archive</span>
|
<span class="app-header__title">ZDDC Archive</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Browse</span>
|
<span class="app-header__title">ZDDC Browse</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 19:19:06 · 054cf2d</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -1979,7 +1979,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
}
|
}
|
||||||
.tg-input:hover { border-color: var(--border); }
|
.tg-input:hover { border-color: var(--border); }
|
||||||
.tg-input:focus { border-color: var(--primary); background: var(--bg); outline: none; }
|
.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-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 { color: var(--text-muted); white-space: nowrap; text-decoration: none; cursor: pointer; }
|
||||||
.tg-orig__link:hover { text-decoration: underline; }
|
.tg-orig__link:hover { text-decoration: underline; }
|
||||||
|
|
@ -2488,7 +2488,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Classifier</span>
|
<span class="app-header__title">ZDDC Classifier</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||||
|
|
@ -2545,6 +2545,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-sm export-list-btn" id="exportListBtn"
|
<button class="btn btn-sm export-list-btn" id="exportListBtn"
|
||||||
title="Copy the filtered file list (path + file columns, no folders) as TSV — paste into Excel, edit, then paste back via “Paste rows”. Paste a full path into the Current name column to bind that exact file.">⬆ Export list</button>
|
title="Copy the filtered file list (path + file columns, no folders) as TSV — paste into Excel, edit, then paste back via “Paste rows”. Paste a full path into the Current name column to bind that exact file.">⬆ Export list</button>
|
||||||
|
<button class="btn btn-sm export-list-btn" id="exportPathsBtn"
|
||||||
|
title="Download the filtered file list as a 1-column CSV of full paths. Feed it to an AI to classify into <party>/<direction>/<transmittal>/<file>.ext, then bring the 2-column result back via “Import paths” above the target list.">⬇ Export paths</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="search" id="treeFilterInput" class="tree-filter" spellcheck="false"
|
<input type="search" id="treeFilterInput" class="tree-filter" spellcheck="false"
|
||||||
placeholder="Filter files… (e.g. master deliverables list)" aria-label="Filter files">
|
placeholder="Filter files… (e.g. master deliverables list)" aria-label="Filter files">
|
||||||
|
|
@ -2567,9 +2569,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
<div class="pane-header-right">
|
<div class="pane-header-right">
|
||||||
<span id="classifyStats" class="file-stats"></span>
|
<span id="classifyStats" class="file-stats"></span>
|
||||||
<span class="header-divider">|</span>
|
<span class="header-divider">|</span>
|
||||||
<button id="exportDatasetBtn" class="btn btn-secondary btn-sm" title="Download the classifications as a filename-per-file JSON to edit (e.g. with an AI), then re-import here. NOT a workspace — no scanned tree.">Export for editing</button>
|
<button id="importPathsBtn" class="btn btn-secondary btn-sm" title="Import a 2-column CSV (old path, new path). Each new path “<party>/<direction>/<transmittal>/<file>.ext” sets that file’s tracking number (rename) and routes it into a transmittal. Only files named in the CSV are touched — others keep their current classification. Export the source list first via “Export paths” on the left.">Import paths…</button>
|
||||||
<button id="importDatasetBtn" class="btn btn-secondary btn-sm" title="Load an edited classification JSON back in — replaces the current classifications. (To move a whole scanned workspace between browsers, use “Import workspace” on the welcome screen.)">Import edits</button>
|
<input type="file" id="importPathsInput" accept=".csv,text/csv,text/plain" hidden>
|
||||||
<input type="file" id="importDatasetInput" accept="application/json,.json" hidden>
|
|
||||||
<button id="resetDatasetBtn" class="btn btn-sm btn-danger" title="Discard all classifications and start over from the raw scanned input (does not touch your files)">Reset</button>
|
<button id="resetDatasetBtn" class="btn btn-sm btn-danger" title="Discard all classifications and start over from the raw scanned input (does not touch your files)">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2591,13 +2592,12 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
</section>
|
</section>
|
||||||
<section id="transmittalPanel" class="target-panel" hidden>
|
<section id="transmittalPanel" class="target-panel" hidden>
|
||||||
<div class="target-panel__toolbar">
|
<div class="target-panel__toolbar">
|
||||||
<button id="addPartyBtn" class="btn btn-sm btn-secondary">+ Party</button>
|
<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>
|
||||||
<span class="target-hint"><party>/{received,issued}/<transmittal>. Drag files (or a whole folder) into a transmittal, then Copy into the archive.</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="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>
|
<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>
|
</div>
|
||||||
<input type="search" id="transmittalFilterInput" class="tree-filter target-filter" spellcheck="false"
|
<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>
|
<div id="transmittalTree" class="target-tree"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -5976,9 +5976,9 @@ X.B(E,Y);return E}return J}())
|
||||||
showExcludedCheckbox: document.getElementById('showExcludedCheckbox'),
|
showExcludedCheckbox: document.getElementById('showExcludedCheckbox'),
|
||||||
showEmptyCheckbox: document.getElementById('showEmptyCheckbox'),
|
showEmptyCheckbox: document.getElementById('showEmptyCheckbox'),
|
||||||
exportListBtn: document.getElementById('exportListBtn'),
|
exportListBtn: document.getElementById('exportListBtn'),
|
||||||
exportDatasetBtn: document.getElementById('exportDatasetBtn'),
|
exportPathsBtn: document.getElementById('exportPathsBtn'),
|
||||||
importDatasetBtn: document.getElementById('importDatasetBtn'),
|
importPathsBtn: document.getElementById('importPathsBtn'),
|
||||||
importDatasetInput: document.getElementById('importDatasetInput'),
|
importPathsInput: document.getElementById('importPathsInput'),
|
||||||
resetDatasetBtn: document.getElementById('resetDatasetBtn'),
|
resetDatasetBtn: document.getElementById('resetDatasetBtn'),
|
||||||
treeFilterInput: document.getElementById('treeFilterInput'),
|
treeFilterInput: document.getElementById('treeFilterInput'),
|
||||||
trackingFilterInput: document.getElementById('trackingFilterInput'),
|
trackingFilterInput: document.getElementById('trackingFilterInput'),
|
||||||
|
|
@ -6045,89 +6045,102 @@ X.B(E,Y);return E}return J}())
|
||||||
(nodes || []).forEach(function (n) { (n.files || []).forEach(cb); walk(n.children); });
|
(nodes || []).forEach(function (n) { (n.files || []).forEach(cb); walk(n.children); });
|
||||||
})(app.folderTree || []);
|
})(app.folderTree || []);
|
||||||
}
|
}
|
||||||
function exportDataset() {
|
// CSV cell quoting (RFC4180): quote when the value holds a comma, quote, or
|
||||||
var c = app.modules.classify, files = [];
|
// newline; embedded quotes are doubled.
|
||||||
eachSourceFile(function (f) {
|
function csvCell(s) { s = (s == null ? '' : String(s)); return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }
|
||||||
var key = c.srcKeyForFile(f);
|
// Minimal RFC4180-ish CSV parser → array of rows of string cells. Handles
|
||||||
var a = c.getAssignment(key) || {};
|
// quoted fields with embedded commas/quotes/newlines (titles may contain
|
||||||
var d = c.deriveTarget(f);
|
// commas). CRLF/CR are normalized to LF.
|
||||||
var rec = {
|
function parseCsv(text) {
|
||||||
source: key,
|
var rows = [], row = [], field = '', inQ = false, i = 0;
|
||||||
originalName: window.zddc.joinExtension(f.originalFilename, f.extension),
|
text = String(text == null ? '' : text).replace(/\r\n?/g, '\n');
|
||||||
filename: a.excluded ? '' : (d.filename || ''),
|
for (; i < text.length; i++) {
|
||||||
excluded: !!a.excluded,
|
var ch = text[i];
|
||||||
};
|
if (inQ) {
|
||||||
if (!a.excluded && a.transmittalNodeId) {
|
if (ch === '"') { if (text[i + 1] === '"') { field += '"'; i++; } else { inQ = false; } }
|
||||||
var t = c.transmittalRecord(a.transmittalNodeId);
|
else { field += ch; }
|
||||||
if (t) rec.transmittal = t;
|
} else if (ch === '"') { inQ = true; }
|
||||||
}
|
else if (ch === ',') { row.push(field); field = ''; }
|
||||||
files.push(rec);
|
else if (ch === '\n') { row.push(field); rows.push(row); row = []; field = ''; }
|
||||||
});
|
else { field += ch; }
|
||||||
var payload = {
|
}
|
||||||
zddcClassifierFiles: 1,
|
if (field !== '' || row.length) { row.push(field); rows.push(row); }
|
||||||
exportedAt: new Date().toISOString(),
|
return rows;
|
||||||
_format: 'One record per input file. Set "filename" to its full ZDDC name '
|
|
||||||
+ '"TRACKING_REV (STATUS) - Title.ext" — on import the app splits TRACKING on "-" and the '
|
|
||||||
+ 'final "_" into nested folders, and files in shared paths share ancestors. Set '
|
|
||||||
+ '"excluded": true for non-documents (filename then ignored). "transmittal" is optional: '
|
|
||||||
+ '{party, slot:"received"|"issued", date:"YYYY-MM-DD", type:"TRN"|"SUB", seq, status, title}. '
|
|
||||||
+ 'Classify every "source" key; do not invent files.',
|
|
||||||
outputName: c.serialize().outputName || null,
|
|
||||||
files: files,
|
|
||||||
};
|
|
||||||
var name = 'classifier-dataset';
|
|
||||||
try {
|
|
||||||
if (app.modules.workspace && typeof app.modules.workspace.activeName === 'function') {
|
|
||||||
name = app.modules.workspace.activeName() || name;
|
|
||||||
}
|
|
||||||
} catch (_) { /* ok */ }
|
|
||||||
var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
|
||||||
var url = URL.createObjectURL(blob);
|
|
||||||
var a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = String(name).replace(/[^\w.-]+/g, '_') + '.zddc-classification.json';
|
|
||||||
document.body.appendChild(a); a.click(); a.remove();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
}
|
||||||
function importDataset(file) {
|
// Trigger a client-side download of `text` as `name`.
|
||||||
|
function downloadText(text, name, mime) {
|
||||||
|
var blob = new Blob([text], { type: mime || 'text/plain' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a'); a.href = url; a.download = name;
|
||||||
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
|
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
|
||||||
|
}
|
||||||
|
// Import a 2-column CSV (old path, new path) — e.g. an AI-classified list.
|
||||||
|
// MERGE semantics: only files named in the CSV are touched; others keep their
|
||||||
|
// current classification. Each new path
|
||||||
|
// "<party>/<direction>/<transmittal>/<file>.ext" drives two axes — the
|
||||||
|
// filename sets the tracking number (rename) and the leading segments route a
|
||||||
|
// transmittal. Either axis can apply independently; per-row problems are
|
||||||
|
// collected and offered as a downloadable errors CSV (the list can be huge).
|
||||||
|
function importPaths(file) {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = function () {
|
reader.onload = function () {
|
||||||
var obj;
|
var rows = parseCsv(reader.result);
|
||||||
try { obj = JSON.parse(reader.result); }
|
if (!rows.length) { window.zddc.toast('Import failed — the CSV is empty.', 'error'); return; }
|
||||||
catch (e) { window.zddc.toast('Import failed — not valid JSON.', 'error'); return; }
|
|
||||||
if (!obj || !Array.isArray(obj.files)) {
|
|
||||||
window.zddc.toast('Import failed — expected a classifier dataset with a "files" list.', 'error'); return;
|
|
||||||
}
|
|
||||||
var c = app.modules.classify;
|
var c = app.modules.classify;
|
||||||
var hasData = c.getTrackingTree().length || c.getTransmittalTree().length
|
// Old path must resolve to a real scanned file (srcKey set).
|
||||||
|| Object.keys(c.serialize().assignments || {}).length;
|
var valid = Object.create(null);
|
||||||
if (hasData && !confirm('Replace the current classification with the imported dataset?')) return;
|
eachSourceFile(function (f) { valid[c.srcKeyForFile(f)] = true; });
|
||||||
c.reset();
|
|
||||||
var ok = 0, bad = 0;
|
var imported = 0, errors = [];
|
||||||
obj.files.forEach(function (rec) {
|
rows.forEach(function (cells, idx) {
|
||||||
if (!rec || !rec.source) return;
|
var oldPath = (cells[0] || '').trim();
|
||||||
var key = rec.source;
|
var newPath = (cells[1] || '').trim();
|
||||||
if (rec.excluded) { c.setExcluded([key], true); ok++; return; }
|
// Tolerate a header row (first row whose first cell isn't a file).
|
||||||
if (rec.filename) {
|
if (idx === 0 && !valid[oldPath] && /^(old|path|source|from)\b/i.test(oldPath)) return;
|
||||||
var p = window.zddc.parseFilename(String(rec.filename).trim());
|
if (!oldPath && !newPath) return; // blank line
|
||||||
if (p && p.valid) {
|
if (!oldPath) { errors.push([oldPath, newPath, 'missing old path']); return; }
|
||||||
var stem = p.trackingNumber + '_' + p.revision + ' (' + p.status + ')';
|
if (!valid[oldPath]) { errors.push([oldPath, newPath, 'no such file in the current scan']); return; }
|
||||||
c.place([key], c.addTrackingPath(null, c.parseFolderLevels(stem)), 'tracking');
|
if (!newPath) { errors.push([oldPath, newPath, 'missing new path']); return; }
|
||||||
if (p.title != null) c.setTitleOverride(key, p.title);
|
|
||||||
ok++;
|
var segs = newPath.split('/').filter(function (s) { return s !== ''; });
|
||||||
} else { bad++; }
|
if (!segs.length) { errors.push([oldPath, newPath, 'empty new path']); return; }
|
||||||
|
var filename = segs[segs.length - 1];
|
||||||
|
var leading = segs.slice(0, -1);
|
||||||
|
var didTracking = false, didTransmittal = false, rowErr = '';
|
||||||
|
function note(m) { rowErr = rowErr ? rowErr + '; ' + m : m; }
|
||||||
|
|
||||||
|
// Axis 1 — filename → tracking tree (the rename).
|
||||||
|
var p = window.zddc.parseFilename(filename);
|
||||||
|
if (p && p.valid) {
|
||||||
|
var stem = p.trackingNumber + '_' + p.revision + ' (' + p.status + ')';
|
||||||
|
c.place([oldPath], c.addTrackingPath(null, c.parseFolderLevels(stem)), 'tracking');
|
||||||
|
if (p.title != null) c.setTitleOverride(oldPath, p.title);
|
||||||
|
didTracking = true;
|
||||||
|
} else {
|
||||||
|
note('filename is not a valid ZDDC name "' + filename + '"');
|
||||||
}
|
}
|
||||||
if (rec.transmittal && rec.transmittal.party) {
|
|
||||||
var t = rec.transmittal;
|
// Axis 2 — <party>/<direction>/<transmittal> → transmittal tree (the
|
||||||
var pid = c.findOrAddParty(t.party);
|
// route). Same parser the By-transmittal grid uses.
|
||||||
var bid = c.findOrAddTransmittalBin(pid, t.slot || 'received', {
|
if (leading.length >= 1) {
|
||||||
date: t.date, type: t.type || 'TRN', seq: t.seq, status: t.status, title: t.title,
|
var terr = c.setTransmittalPath([oldPath], leading.join('/'));
|
||||||
});
|
if (terr) note(terr); else didTransmittal = true;
|
||||||
if (bid) c.place([key], bid, 'transmittal');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (didTracking || didTransmittal) imported++;
|
||||||
|
if (rowErr) errors.push([oldPath, newPath, rowErr]);
|
||||||
});
|
});
|
||||||
window.zddc.toast('Imported ' + ok + ' file' + (ok === 1 ? '' : 's')
|
|
||||||
+ (bad ? (' — ' + bad + ' had an unparseable filename') : '') + '.', bad ? 'warning' : 'success');
|
if (errors.length) {
|
||||||
|
var elines = ['old path,new path,reason'];
|
||||||
|
errors.forEach(function (e) { elines.push(csvCell(e[0]) + ',' + csvCell(e[1]) + ',' + csvCell(e[2])); });
|
||||||
|
downloadText(elines.join('\n'), 'classifier-import-errors.csv', 'text/csv');
|
||||||
|
}
|
||||||
|
window.zddc.toast('Imported ' + imported + ' file' + (imported === 1 ? '' : 's')
|
||||||
|
+ (errors.length ? (' — ' + errors.length + ' row' + (errors.length === 1 ? '' : 's')
|
||||||
|
+ ' had problems (downloaded classifier-import-errors.csv)') : '') + '.',
|
||||||
|
errors.length ? 'warning' : 'success');
|
||||||
};
|
};
|
||||||
reader.onerror = function () { window.zddc.toast('Import failed — could not read the file.', 'error'); };
|
reader.onerror = function () { window.zddc.toast('Import failed — could not read the file.', 'error'); };
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
|
@ -6207,11 +6220,13 @@ X.B(E,Y);return E}return J}())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dataset export / import (round-trip the classification through a JSON file).
|
// Dataset export / import (round-trip the classification through a JSON file).
|
||||||
if (app.dom.exportDatasetBtn) app.dom.exportDatasetBtn.addEventListener('click', exportDataset);
|
if (app.dom.exportPathsBtn) app.dom.exportPathsBtn.addEventListener('click', function () {
|
||||||
if (app.dom.importDatasetBtn) app.dom.importDatasetBtn.addEventListener('click', function () { app.dom.importDatasetInput.click(); });
|
if (app.modules.tree && app.modules.tree.exportPathList) app.modules.tree.exportPathList();
|
||||||
|
});
|
||||||
|
if (app.dom.importPathsBtn) app.dom.importPathsBtn.addEventListener('click', function () { app.dom.importPathsInput.click(); });
|
||||||
if (app.dom.resetDatasetBtn) app.dom.resetDatasetBtn.addEventListener('click', resetDataset);
|
if (app.dom.resetDatasetBtn) app.dom.resetDatasetBtn.addEventListener('click', resetDataset);
|
||||||
if (app.dom.importDatasetInput) app.dom.importDatasetInput.addEventListener('change', function () {
|
if (app.dom.importPathsInput) app.dom.importPathsInput.addEventListener('change', function () {
|
||||||
if (this.files && this.files[0]) importDataset(this.files[0]);
|
if (this.files && this.files[0]) importPaths(this.files[0]);
|
||||||
this.value = ''; // allow re-importing the same file
|
this.value = ''; // allow re-importing the same file
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -7430,6 +7445,7 @@ X.B(E,Y);return E}return J}())
|
||||||
config: defaultConfig(), // tracking-number pattern (fields/statuses/modifiers)
|
config: defaultConfig(), // tracking-number pattern (fields/statuses/modifiers)
|
||||||
worklist: [], // "From a list" scratch rows: [ { id, trackingNumber, title, revisionCell, source, archiveRevisions } ]
|
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)
|
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 }
|
// id -> { node, kind:'tracking'|'party'|'slot'|'transmittal', parent }
|
||||||
|
|
@ -7791,6 +7807,7 @@ X.B(E,Y);return E}return J}())
|
||||||
return { id: r.id, party: r.party, trackingNumber: r.trackingNumber, title: r.title, revisionCell: r.revisionCell, source: r.source, archiveRevisions: r.archiveRevisions };
|
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),
|
trackingWorkset: Object.keys(state.trackingWorkset),
|
||||||
|
transmittalWorkset: Object.keys(state.transmittalWorkset),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function load(obj) {
|
function load(obj) {
|
||||||
|
|
@ -7803,6 +7820,8 @@ X.B(E,Y);return E}return J}())
|
||||||
state.worklist = (Array.isArray(obj.worklist) ? obj.worklist : []).map(normalizeRow);
|
state.worklist = (Array.isArray(obj.worklist) ? obj.worklist : []).map(normalizeRow);
|
||||||
state.trackingWorkset = Object.create(null);
|
state.trackingWorkset = Object.create(null);
|
||||||
(Array.isArray(obj.trackingWorkset) ? obj.trackingWorkset : []).forEach(function (k) { state.trackingWorkset[k] = true; });
|
(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();
|
rebuildIndex();
|
||||||
migrateLegacyMdl(obj.worklist); // BEFORE anything can prune; materializes old mdl placements
|
migrateLegacyMdl(obj.worklist); // BEFORE anything can prune; materializes old mdl placements
|
||||||
notify();
|
notify();
|
||||||
|
|
@ -7835,6 +7854,7 @@ X.B(E,Y);return E}return J}())
|
||||||
state.assignments = {}; state.trackingTree = []; state.transmittalTree = [];
|
state.assignments = {}; state.trackingTree = []; state.transmittalTree = [];
|
||||||
state.outputName = null;
|
state.outputName = null;
|
||||||
state.trackingWorkset = Object.create(null);
|
state.trackingWorkset = Object.create(null);
|
||||||
|
state.transmittalWorkset = Object.create(null);
|
||||||
rebuildIndex();
|
rebuildIndex();
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
@ -7898,6 +7918,84 @@ X.B(E,Y);return E}return J}())
|
||||||
notify();
|
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 ───────────────────────────────────────────────────────
|
// ── pattern config ───────────────────────────────────────────────────────
|
||||||
function normalizeConfig(c) {
|
function normalizeConfig(c) {
|
||||||
var d = defaultConfig();
|
var d = defaultConfig();
|
||||||
|
|
@ -8352,6 +8450,9 @@ X.B(E,Y);return E}return J}())
|
||||||
// By-tracking grid
|
// By-tracking grid
|
||||||
addToTrackingGrid: addToTrackingGrid, removeFromTrackingGrid: removeFromTrackingGrid,
|
addToTrackingGrid: addToTrackingGrid, removeFromTrackingGrid: removeFromTrackingGrid,
|
||||||
trackingGridKeys: trackingGridKeys, setFileIdentity: setFileIdentity, forgetFile: forgetFile,
|
trackingGridKeys: trackingGridKeys, setFileIdentity: setFileIdentity, forgetFile: forgetFile,
|
||||||
|
// By-transmittal grid
|
||||||
|
addToTransmittalGrid: addToTransmittalGrid, removeFromTransmittalGrid: removeFromTransmittalGrid,
|
||||||
|
transmittalGridKeys: transmittalGridKeys, setTransmittalPath: setTransmittalPath,
|
||||||
setWorklist: setWorklist, appendWorklist: appendWorklist, clearWorklist: clearWorklist,
|
setWorklist: setWorklist, appendWorklist: appendWorklist, clearWorklist: clearWorklist,
|
||||||
removeWorklistRow: removeWorklistRow,
|
removeWorklistRow: removeWorklistRow,
|
||||||
getWorklist: getWorklist, getWorklistRow: getWorklistRow,
|
getWorklist: getWorklist, getWorklistRow: getWorklistRow,
|
||||||
|
|
@ -10316,6 +10417,27 @@ X.B(E,Y);return E}return J}())
|
||||||
if (!built.count) { window.zddc.toast('No files to export — nothing passes the current filters.', 'info'); return; }
|
if (!built.count) { window.zddc.toast('No files to export — nothing passes the current filters.', 'info'); return; }
|
||||||
copyOrDownload(built.tsv, built.count);
|
copyOrDownload(built.tsv, built.count);
|
||||||
}
|
}
|
||||||
|
// Download the filtered file list as a 1-column CSV of full (root-relative)
|
||||||
|
// paths — the same keys “Import paths” matches on. Meant to be handed to an AI
|
||||||
|
// that returns a 2-column old→new mapping.
|
||||||
|
function exportPathList() {
|
||||||
|
var c = window.app.modules.classify;
|
||||||
|
var files = filteredFileObjects().slice().sort(function (a, b) {
|
||||||
|
return cmpName(c.srcKeyForFile(a), c.srcKeyForFile(b));
|
||||||
|
});
|
||||||
|
if (!files.length) { window.zddc.toast('No files to export — nothing passes the current filters.', 'info'); return; }
|
||||||
|
function cell(s) { s = (s == null ? '' : String(s)); return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }
|
||||||
|
var lines = ['path'];
|
||||||
|
files.forEach(function (f) { lines.push(cell(c.srcKeyForFile(f))); });
|
||||||
|
try {
|
||||||
|
var blob = new Blob([lines.join('\n')], { type: 'text/csv' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a'); a.href = url; a.download = 'classifier-paths.csv';
|
||||||
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
|
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
|
||||||
|
window.zddc.toast('Exported ' + files.length + ' path' + (files.length === 1 ? '' : 's') + ' to classifier-paths.csv.', 'success');
|
||||||
|
} catch (e) { window.zddc.toast('Could not export the path list — ' + (e.message || e), 'error'); }
|
||||||
|
}
|
||||||
function copyOrDownload(text, count) {
|
function copyOrDownload(text, count) {
|
||||||
function ok() { window.zddc.toast('Copied ' + count + ' file' + (count === 1 ? '' : 's') + ' (path + file) — paste into Excel.', 'success'); }
|
function ok() { window.zddc.toast('Copied ' + count + ' file' + (count === 1 ? '' : 's') + ' (path + file) — paste into Excel.', 'success'); }
|
||||||
function download() {
|
function download() {
|
||||||
|
|
@ -11155,6 +11277,7 @@ X.B(E,Y);return E}return J}())
|
||||||
setShowFilters,
|
setShowFilters,
|
||||||
setNameFilter,
|
setNameFilter,
|
||||||
exportFilteredList,
|
exportFilteredList,
|
||||||
|
exportPathList,
|
||||||
filteredFiles: filteredFileObjects,
|
filteredFiles: filteredFileObjects,
|
||||||
_buildExportTsv: buildExportTsv
|
_buildExportTsv: buildExportTsv
|
||||||
};
|
};
|
||||||
|
|
@ -11301,23 +11424,22 @@ X.B(E,Y);return E}return J}())
|
||||||
/**
|
/**
|
||||||
* ZDDC Classifier — target-tree pane (Classify & Copy mode).
|
* ZDDC Classifier — target-tree pane (Classify & Copy mode).
|
||||||
*
|
*
|
||||||
* Renders the two orthogonal target trees the user maps files onto:
|
* Two orthogonal per-file grids (both on the shared seltable) the user maps
|
||||||
* - "By tracking number": folders that join with "-" into the tracking
|
* files onto — one editable row per file:
|
||||||
* number; the leaf folder ("A (IFR)") is the revision+status.
|
* - "By tracking number": Tracking# / Rev (Status) / Title cells compose the
|
||||||
* - "By transmittal": <party>/{received,issued}/<transmittal folder>.
|
* 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
|
* Structure + placements live in classify.js; everything shown here is derived,
|
||||||
* in source-dnd.js / phase 3; this module owns rendering + folder/bin CRUD and
|
* never stored. Drops are handled per-grid (setupGridDrop / setupTransmittalDrop)
|
||||||
* shows the derived filename for each placed file.
|
* so a ⌘/Ctrl transmittal drop can branch a new folder.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var SLOTS = ['received', 'issued'];
|
|
||||||
|
|
||||||
var els = {};
|
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 initialized = false;
|
||||||
var currentTab = 'tracking'; // 'tracking' | 'transmittal' — active tab
|
var currentTab = 'tracking'; // 'tracking' | 'transmittal' — active tab
|
||||||
var listScanned = false; // a Load has run this session (drives the "new" badge)
|
var listScanned = false; // a Load has run this session (drives the "new" badge)
|
||||||
|
|
@ -11339,7 +11461,6 @@ X.B(E,Y);return E}return J}())
|
||||||
addFilteredBtn: document.getElementById('addFilteredBtn'),
|
addFilteredBtn: document.getElementById('addFilteredBtn'),
|
||||||
renameBtn: document.getElementById('renameBtn'),
|
renameBtn: document.getElementById('renameBtn'),
|
||||||
trackingColsBtn: document.getElementById('trackingColsBtn'),
|
trackingColsBtn: document.getElementById('trackingColsBtn'),
|
||||||
addPartyBtn: document.getElementById('addPartyBtn'),
|
|
||||||
stats: document.getElementById('classifyStats'),
|
stats: document.getElementById('classifyStats'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -11370,16 +11491,9 @@ X.B(E,Y);return E}return J}())
|
||||||
if (text) { e.preventDefault(); openPasteDialog(text); }
|
if (text) { e.preventDefault(); openPasteDialog(text); }
|
||||||
});
|
});
|
||||||
if (els.trackingColsBtn) els.trackingColsBtn.addEventListener('click', openColumnChooser);
|
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);
|
setupGridDrop(els.trackingTree);
|
||||||
setupDropZone(els.transmittalTree, 'transmittal');
|
setupTransmittalDrop(els.transmittalTree);
|
||||||
|
|
||||||
C().on(render);
|
C().on(render);
|
||||||
if (window.app.modules.store && window.app.modules.store.on) {
|
if (window.app.modules.store && window.app.modules.store.on) {
|
||||||
|
|
@ -11401,24 +11515,6 @@ X.B(E,Y);return E}return J}())
|
||||||
})(window.app.folderTree || []);
|
})(window.app.folderTree || []);
|
||||||
return out;
|
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) {
|
function showTab(which) {
|
||||||
currentTab = (which === 'transmittal') ? 'transmittal' : 'tracking';
|
currentTab = (which === 'transmittal') ? 'transmittal' : 'tracking';
|
||||||
els.trackingTab.classList.toggle('active', currentTab === 'tracking');
|
els.trackingTab.classList.toggle('active', currentTab === 'tracking');
|
||||||
|
|
@ -11438,9 +11534,8 @@ X.B(E,Y);return E}return J}())
|
||||||
function render() {
|
function render() {
|
||||||
if (!initialized || !C().isEnabled()) return;
|
if (!initialized || !C().isEnabled()) return;
|
||||||
var files = allFiles();
|
var files = allFiles();
|
||||||
var placed = buildPlaced(files);
|
|
||||||
renderTrackingGrid(els.trackingTree);
|
renderTrackingGrid(els.trackingTree);
|
||||||
renderTransmittalInto(els.transmittalTree, C().getTransmittalTree(), placed.transmittal);
|
renderTransmittalGrid(els.transmittalTree);
|
||||||
renderStats(files);
|
renderStats(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -11484,66 +11579,13 @@ X.B(E,Y);return E}return J}())
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeActions(extra) {
|
// ── name filter (the autofilter box above the target grids) ────────────
|
||||||
var wrap = el('span', 'tnode__actions');
|
// Mirrored into each grid's own global filter (seltable.setFilter) on render.
|
||||||
(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) ────────────
|
|
||||||
var rfTerms = [];
|
var rfTerms = [];
|
||||||
function setNameFilter(q) {
|
function setNameFilter(q) {
|
||||||
rfTerms = String(q || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
rfTerms = String(q || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
render();
|
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
|
// ── By-tracking: flat editable grid (one row per file), on the shared
|
||||||
// seltable — so it gets multi-sort + per-column autofilters + resizable,
|
// seltable — so it gets multi-sort + per-column autofilters + resizable,
|
||||||
// persisted widths for free. Only `hidden` (the Columns ▾ chooser) is
|
// persisted widths for free. Only `hidden` (the Columns ▾ chooser) is
|
||||||
|
|
@ -11831,93 +11873,85 @@ X.B(E,Y);return E}return J}())
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transmittal tree
|
// ── By-transmittal: flat editable grid (one row per file), mirroring the
|
||||||
function renderTransmittalInto(container, parties, placedMap) {
|
// By-tracking grid. Each row's single text input is the file's full
|
||||||
container.textContent = '';
|
// transmittal folder path "<party>/<received|issued>/<folder>"; committing
|
||||||
if (!parties.length) {
|
// it routes the file (classify.setTransmittalPath find-or-creates the
|
||||||
container.appendChild(el('div', 'target-empty', 'No parties yet — “+ Party” to start.'));
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
parties.forEach(function (p) { var e = partyNode(p, placedMap); if (e) container.appendChild(e); });
|
ensureTransmittalGrid(container);
|
||||||
if (rfActive() && !container.children.length) {
|
transmittalGrid.setFilter(rfTerms.join(' '));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var STATUSES = ['---', 'IFA', 'IFB', 'IFC', 'IFD', 'IFI', 'IFP', 'IFR', 'IFU', 'REC', 'RSA', 'RSB', 'RSC', 'RSD', 'RSI', 'TBD'];
|
function setGridStatus(text) {
|
||||||
function binForm(partyId, slot) {
|
var s = document.getElementById('scanStatus');
|
||||||
var form = el('div', 'binform');
|
if (s) { s.textContent = text; s.classList.toggle('scanning', !!text); }
|
||||||
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) {
|
function setGridStatus(text) {
|
||||||
|
|
@ -12237,163 +12271,75 @@ X.B(E,Y);return E}return J}())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── events ─────────────────────────────────────────────────────────────
|
// ── events ─────────────────────────────────────────────────────────────
|
||||||
function closestNodeId(target) {
|
|
||||||
var n = target.closest('[data-id]');
|
|
||||||
return n ? n.dataset.id : null;
|
|
||||||
}
|
|
||||||
function fileByKey(key) {
|
function fileByKey(key) {
|
||||||
var files = allFiles();
|
var files = allFiles();
|
||||||
for (var i = 0; i < files.length; i++) { if (C().srcKeyForFile(files[i]) === key) return files[i]; }
|
for (var i = 0; i < files.length; i++) { if (C().srcKeyForFile(files[i]) === key) return files[i]; }
|
||||||
return null;
|
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') {
|
// ── By-transmittal drops ─────────────────────────────────────────────────
|
||||||
var slotEl = btn.closest('.tslot');
|
// Handled at the container level (not seltable's per-row onRowDrop) so the
|
||||||
openForm = { partyId: slotEl.dataset.party, slot: slotEl.dataset.slot };
|
// drop event's modifier key is available:
|
||||||
render();
|
// plain drop on a routed row → the dropped files JOIN that row's folder.
|
||||||
return;
|
// ⌘/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) {
|
container.addEventListener('dragover', function (e) {
|
||||||
if (!window.app.modules.dnd.active()) return;
|
if (!window.app.modules.dnd.active()) return;
|
||||||
var t = dropTarget(e.target, axis);
|
e.preventDefault(); e.dataTransfer.dropEffect = 'copy';
|
||||||
clearHover(container);
|
clearHover();
|
||||||
if (!t) return;
|
var tr = rowUnder(e);
|
||||||
e.preventDefault();
|
if (tr) tr.classList.add('drop-hover'); else container.classList.add('tg-drop-hover');
|
||||||
e.dataTransfer.dropEffect = 'copy';
|
|
||||||
t.row.classList.add('drop-hover');
|
|
||||||
});
|
|
||||||
container.addEventListener('dragleave', function (e) {
|
|
||||||
if (e.target === container) clearHover(container);
|
|
||||||
});
|
});
|
||||||
|
container.addEventListener('dragleave', function (e) { if (e.target === container) clearHover(); });
|
||||||
container.addEventListener('drop', function (e) {
|
container.addEventListener('drop', function (e) {
|
||||||
var t = dropTarget(e.target, axis);
|
|
||||||
clearHover(container);
|
|
||||||
if (!t) return;
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
var tr = rowUnder(e), meta = e.metaKey || e.ctrlKey;
|
||||||
|
clearHover();
|
||||||
var keys = window.app.modules.dnd.getDrag();
|
var keys = window.app.modules.dnd.getDrag();
|
||||||
window.app.modules.dnd.clearDrag();
|
window.app.modules.dnd.clearDrag();
|
||||||
if (!keys.length) return;
|
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).
|
// Reveal a source key's placement in the target pane (source → target).
|
||||||
function reveal(key) {
|
function reveal(key) {
|
||||||
var a = C().getAssignment(key);
|
var a = C().getAssignment(key);
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
|
// Both tabs are per-file grids whose rows are keyed "f:<srcKey>".
|
||||||
if (a.trackingNodeId) {
|
if (a.trackingNodeId) {
|
||||||
showTab('tracking'); collapsed = {}; render();
|
showTab('tracking'); render();
|
||||||
flashNode(els.trackingTree, a.trackingNodeId);
|
flashNode(els.trackingTree, 'f:' + key);
|
||||||
} else if (a.transmittalNodeId) {
|
} else if (a.transmittalNodeId) {
|
||||||
showTab('transmittal'); render();
|
showTab('transmittal'); render();
|
||||||
flashNode(els.transmittalTree, a.transmittalNodeId);
|
flashNode(els.transmittalTree, 'f:' + key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function flashNode(container, id) {
|
function flashNode(container, id) {
|
||||||
|
|
|
||||||
|
|
@ -1793,7 +1793,7 @@ body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC</span>
|
<span class="app-header__title">ZDDC</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -2770,7 +2770,7 @@ dialog.modal--narrow {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Transmittal</span>
|
<span class="app-header__title">ZDDC Transmittal</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-18 15:12:58 · 9ca24eb</span></span>
|
||||||
</div>
|
</div>
|
||||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||||
<!-- Publish split-button (Transmittal-specific primary action;
|
<!-- Publish split-button (Transmittal-specific primary action;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||||
archive=v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
|
archive=v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb
|
||||||
transmittal=v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
|
transmittal=v0.0.27-beta · 2026-06-18 15:12:58 · 9ca24eb
|
||||||
classifier=v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
|
classifier=v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb
|
||||||
landing=v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
|
landing=v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb
|
||||||
form=v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
|
form=v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb
|
||||||
tables=v0.0.27-beta · 2026-06-16 19:19:06 · 054cf2d
|
tables=v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb
|
||||||
browse=v0.0.27-beta · 2026-06-16 19:19:06 · 054cf2d
|
browse=v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb
|
||||||
|
|
|
||||||
|
|
@ -1780,7 +1780,7 @@ body.is-elevated::after {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 19:19:06 · 054cf2d</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-18 15:12:59 · 9ca24eb</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue