ZDDC/classifier/js/mdl-instantiate.js
2026-06-11 13:32:31 -05:00

209 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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,
};
})();