209 lines
10 KiB
JavaScript
209 lines
10 KiB
JavaScript
/**
|
||
* ZDDC Classifier — instantiate MDL deliverables from existing archive files.
|
||
*
|
||
* Catch-up flow: the archive already holds issued documents, but the Master
|
||
* Deliverables List is empty. This reads a project's archive subtree as a flat
|
||
* file list, lets the user build a selection set (autofilter + ctrl-shift via
|
||
* the shared seltable), dedupes the selected files to one deliverable per
|
||
* tracking number, and PUTs a new deliverable .yaml into the originator's
|
||
* `archive/<originator>/mdl/` on the server. Server-only (needs http + auth).
|
||
*
|
||
* A deliverable .yaml's filename IS its tracking number; the server pins
|
||
* `originator` from the folder and composes the filename, so the body carries
|
||
* only project/discipline/type/sequence/suffix + title.
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
if (!window.app) window.app = {};
|
||
if (!window.app.modules) window.app.modules = {};
|
||
|
||
function T(m, l, o) { if (window.zddc && window.zddc.toast) window.zddc.toast(m, l, o); }
|
||
function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }
|
||
|
||
// ── pure core (test seams) ───────────────────────────────────────────────
|
||
|
||
// A tracking number → deliverable {tracking, originator, body{...}} or null
|
||
// if it doesn't fit the MDL schema (needs orig-proj-disc-type-seq, + suffix).
|
||
function deliverableFromFile(f) {
|
||
var segs = String(f.tracking || '').split('-');
|
||
if (segs.length < 5) return null;
|
||
var body = { project: segs[1], discipline: segs[2], type: segs[3], sequence: segs[4], title: f.title || '' };
|
||
if (segs.length >= 6) body.suffix = segs.slice(5).join('-');
|
||
return { tracking: f.tracking, originator: segs[0], body: body };
|
||
}
|
||
// Dedupe a list of archive files to one deliverable per tracking number.
|
||
function dedupe(files) {
|
||
var seen = Object.create(null), out = [];
|
||
(files || []).forEach(function (f) {
|
||
if (seen[f.tracking]) return;
|
||
var d = deliverableFromFile(f);
|
||
if (d) { seen[f.tracking] = true; out.push(d); }
|
||
});
|
||
return out;
|
||
}
|
||
|
||
// Recursively walk an archive directory handle → flat list of ZDDC-named
|
||
// files (skips dot/underscore folders; non-ZDDC names like the mdl yamls
|
||
// naturally fall out because parseFilename rejects them).
|
||
async function walkArchive(rootHandle) {
|
||
var out = [];
|
||
async function walk(dirH, parts) {
|
||
for await (var entry of dirH.values()) {
|
||
var nm = String(entry.name || '').replace(/\/$/, '');
|
||
if (entry.kind === 'directory') {
|
||
var c = nm.charAt(0);
|
||
if (c === '.' || c === '_' || nm === 'mdl' || nm === 'rsk') continue;
|
||
var childH = await dirH.getDirectoryHandle(nm);
|
||
await walk(childH, parts.concat(nm));
|
||
} else {
|
||
var p = window.zddc.parseFilename(nm);
|
||
if (p && p.valid) {
|
||
out.push({
|
||
id: parts.concat(nm).join('/'),
|
||
party: parts[0] || '', slot: parts[1] || '', transmittal: parts[2] || '',
|
||
name: nm, tracking: p.trackingNumber, revision: p.revision, status: p.status, title: p.title,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
await walk(rootHandle, []);
|
||
return out;
|
||
}
|
||
|
||
// Write one deliverable into <archiveRoot>/<originator>/mdl/<tracking>.yaml.
|
||
// Returns 'created' | 'skipped' (already present). Throws on server error.
|
||
async function instantiateOne(archiveRoot, d) {
|
||
var dir = await archiveRoot.getDirectoryHandle(d.originator, { create: true });
|
||
dir = await dir.getDirectoryHandle('mdl', { create: true });
|
||
var fname = d.tracking + '.yaml';
|
||
try { await dir.getFileHandle(fname); return 'skipped'; } catch (e) { /* NotFound → create */ }
|
||
var yaml = window.jsyaml.dump(d.body);
|
||
var fh = await dir.getFileHandle(fname, { create: true });
|
||
var w = await fh.createWritable();
|
||
await w.write(new Blob([yaml], { type: 'application/yaml' }));
|
||
await w.close();
|
||
return 'created';
|
||
}
|
||
|
||
async function instantiateAll(archiveRoot, deliverables, onProgress) {
|
||
var s = { created: 0, skipped: 0, errors: 0 };
|
||
for (var i = 0; i < deliverables.length; i++) {
|
||
if (onProgress) onProgress(i + 1, deliverables.length, deliverables[i].tracking);
|
||
try { s[await instantiateOne(archiveRoot, deliverables[i])]++; }
|
||
catch (e) { s.errors++; T('Failed to create ' + deliverables[i].tracking + ' — ' + (e.message || e), 'error'); }
|
||
}
|
||
return s;
|
||
}
|
||
|
||
// ── UI ───────────────────────────────────────────────────────────────────
|
||
var overlay = null, statusEl = null, table = null, files = [], archiveRoot = null;
|
||
|
||
function close() { if (overlay) { overlay.remove(); overlay = null; table = null; } }
|
||
function setStatus(t) { if (statusEl) statusEl.textContent = t; }
|
||
|
||
async function open() {
|
||
var copy = window.app.modules.copy;
|
||
var src = window.zddc && window.zddc.source;
|
||
if (!src || location.protocol === 'file:') {
|
||
T('Populating the MDL from the archive needs the classifier served by a zddc-server (open it over http).', 'error');
|
||
return;
|
||
}
|
||
var projects = await copy.fetchAccessProjects();
|
||
if (projects == null) { T('Could not load your projects from the server.', 'error'); return; }
|
||
if (!projects.length) { T('No projects you can access on this server.', 'warning'); return; }
|
||
var proj = await copy.chooseProject(projects);
|
||
if (!proj) return;
|
||
buildOverlay(proj);
|
||
await scan(proj);
|
||
}
|
||
|
||
function buildOverlay(proj) {
|
||
close();
|
||
overlay = el('div', 'mdl-overlay');
|
||
var box = el('div', 'mdl-overlay__box');
|
||
var head = el('div', 'mdl-overlay__head');
|
||
head.appendChild(el('h2', null, 'Populate MDL from archive — ' + (proj.title || proj.name)));
|
||
var x = el('button', 'mdl-overlay__close', '×'); x.title = 'Close'; x.addEventListener('click', close);
|
||
head.appendChild(x);
|
||
box.appendChild(head);
|
||
statusEl = el('div', 'mdl-overlay__status', 'Scanning archive…');
|
||
box.appendChild(statusEl);
|
||
var host = el('div', 'mdl-overlay__table');
|
||
box.appendChild(host);
|
||
var foot = el('div', 'mdl-overlay__foot');
|
||
var create = el('button', 'btn btn-primary', 'Create deliverables');
|
||
create.addEventListener('click', function () { runCreate(create); });
|
||
foot.appendChild(create);
|
||
var cancel = el('button', 'btn btn-secondary', 'Close'); cancel.addEventListener('click', close);
|
||
foot.appendChild(cancel);
|
||
box.appendChild(foot);
|
||
overlay.appendChild(box);
|
||
document.body.appendChild(overlay);
|
||
|
||
table = window.app.modules.seltable.create({
|
||
container: host,
|
||
filterPlaceholder: 'Filter by party, transmittal, tracking number, title…',
|
||
rows: function () { return files; },
|
||
rowId: function (r) { return r.id; },
|
||
columns: [
|
||
{ key: 'party', title: 'Party' },
|
||
{ key: 'slot', title: 'Slot' },
|
||
{ key: 'transmittal', title: 'Transmittal' },
|
||
{ key: 'tracking', title: 'Tracking number' },
|
||
{ key: 'revision', title: 'Rev', get: function (r) { return r.revision + (r.status ? ' (' + r.status + ')' : ''); } },
|
||
{ key: 'title', title: 'Title' },
|
||
],
|
||
onSelectionChange: function (ids) { create.textContent = ids.length ? ('Create deliverables (' + dedupe(selectedFiles(ids)).length + ')') : 'Create deliverables'; },
|
||
});
|
||
table.render();
|
||
}
|
||
|
||
function selectedFiles(ids) {
|
||
var set = {}; ids.forEach(function (i) { set[i] = true; });
|
||
return files.filter(function (f) { return set[f.id]; });
|
||
}
|
||
|
||
async function scan(proj) {
|
||
var src = window.zddc.source;
|
||
var rel = (proj.url || ('/' + proj.name + '/'));
|
||
if (rel.charAt(rel.length - 1) !== '/') rel += '/';
|
||
try {
|
||
archiveRoot = new src.HttpDirectoryHandle(new URL(rel + 'archive/', location.origin).href, 'archive');
|
||
setStatus('Scanning archive…');
|
||
files = await walkArchive(archiveRoot);
|
||
table.renderBody();
|
||
setStatus(files.length + ' document file' + (files.length === 1 ? '' : 's') + ' found. Filter + ctrl-shift select, then “Create deliverables”.');
|
||
} catch (e) {
|
||
setStatus('Archive scan failed — ' + (e.message || e));
|
||
T('Archive scan failed — ' + (e.message || e), 'error');
|
||
}
|
||
}
|
||
|
||
async function runCreate(btn) {
|
||
if (!table) return;
|
||
var sel = table.getSelection();
|
||
if (!sel.length) { T('Select some archive files first (filter + ctrl-shift).', 'warning'); return; }
|
||
var deliverables = dedupe(selectedFiles(sel));
|
||
if (!deliverables.length) { T('None of the selected files have a tracking number that fits the deliverable schema.', 'warning'); return; }
|
||
if (!confirm('Create ' + deliverables.length + ' deliverable(s) in the project MDL?\n\n'
|
||
+ 'One .yaml per tracking number, in archive/<originator>/mdl/. Already-present ones are skipped.')) return;
|
||
btn.disabled = true;
|
||
var s = await instantiateAll(archiveRoot, deliverables, function (i, n, tn) { setStatus('Creating ' + i + '/' + n + ' — ' + tn); });
|
||
btn.disabled = false;
|
||
setStatus(s.created + ' created, ' + s.skipped + ' already there'
|
||
+ (s.errors ? (', ' + s.errors + ' failed') : '') + '. ' + files.length + ' files scanned.');
|
||
T('MDL: ' + s.created + ' created, ' + s.skipped + ' already there'
|
||
+ (s.errors ? (', ' + s.errors + ' failed') : '') + '.', s.errors ? 'warning' : 'success');
|
||
}
|
||
|
||
window.app.modules.mdlInstantiate = {
|
||
open: open,
|
||
// test seams
|
||
deliverableFromFile: deliverableFromFile,
|
||
dedupe: dedupe,
|
||
walkArchive: walkArchive,
|
||
instantiateOne: instantiateOne,
|
||
instantiateAll: instantiateAll,
|
||
};
|
||
})();
|