// mdl-from-archive.js — "Add from archive" for the project MDL rollup. // // The MDL owns the workflow of registering deliverables; this is the catch-up // path. On the project rollup (/mdl/), walk the project archive into a // shared seltable (autofilter + ctrl-shift selection), dedupe the selection to // one deliverable per tracking number, and PUT a deliverable .yaml into each // originator's archive//mdl/. The body's identity fields are split // from the tracking number positionally per the project's own table columns // (originator is folder-pinned, so omitted); the server composes/validates the // filename. Server-only. (function (app) { 'use strict'; 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; } function ctxObj() { return (app && app.context) || {}; } // The tracking-number identity fields, in order, from the table columns: // everything between `originator` and `title` (e.g. phase, project, area, // discipline, type, sequence, suffix). originator is folder-pinned. function identityFields() { var cols = (ctxObj().columns || []).map(function (c) { return c && c.field; }).filter(Boolean); var oi = cols.indexOf('originator'), ti = cols.indexOf('title'); return cols.slice(oi >= 0 ? oi + 1 : 0, ti >= 0 ? ti : cols.length); } // tracking → { tracking, originator, body{identity fields + title} }, or null // if it can't supply the originator + at least one identity segment. function deliverableFromFile(f, idFields) { var segs = String(f.tracking || '').split('-'); if (segs.length < 2) return null; var rest = segs.slice(1), body = {}; idFields.forEach(function (name, i) { if (rest[i] != null && rest[i] !== '') body[name] = rest[i]; }); if (!Object.keys(body).length) return null; body.title = f.title || ''; return { tracking: f.tracking, originator: segs[0], body: body }; } function dedupe(files, idFields) { var seen = Object.create(null), out = []; (files || []).forEach(function (f) { if (seen[f.tracking]) return; var d = deliverableFromFile(f, idFields); if (d) { seen[f.tracking] = true; out.push(d); } }); return out; } 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; await walk(await dirH.getDirectoryHandle(nm), parts.concat(nm)); } else { var p = window.zddc.parseFilename(nm); if (p && p.valid && p.trackingNumber) { out.push({ id: parts.concat(nm).join('/'), party: parts[0] || '', slot: parts[1] || '', transmittal: parts[2] || '', tracking: p.trackingNumber, revision: p.revision, status: p.status, title: p.title, }); } } } } await walk(rootHandle, []); return out; } 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 fh = await dir.getFileHandle(fname, { create: true }); var w = await fh.createWritable(); await w.write(new Blob([window.jsyaml.dump(d.body)], { type: 'application/yaml' })); await w.close(); return 'created'; } // ── 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; } function archiveBaseUrl() { var proj = (location.pathname || '/').replace(/\/mdl\/.*$/, '/'); // / return location.origin + proj + 'archive/'; } async function open() { var src = window.zddc && window.zddc.source; if (!src || (location.protocol !== 'http:' && location.protocol !== 'https:')) { T('Adding from the archive needs the tables page served by a zddc-server.', 'error'); return; } buildOverlay(); try { archiveRoot = new src.HttpDirectoryHandle(archiveBaseUrl(), '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'); } } function buildOverlay() { close(); overlay = el('div', 'mdlarch-overlay'); var box = el('div', 'mdlarch-overlay__box'); var head = el('div', 'mdlarch-overlay__head'); head.appendChild(el('h2', null, 'Add deliverables from archive')); var x = el('button', 'mdlarch-overlay__close', '×'); x.title = 'Close'; x.addEventListener('click', close); head.appendChild(x); box.appendChild(head); statusEl = el('div', 'mdlarch-overlay__status', 'Scanning archive…'); box.appendChild(statusEl); var host = el('div', 'mdlarch-overlay__table'); box.appendChild(host); var foot = el('div', 'mdlarch-overlay__foot'); var create = el('button', 'btn btn-primary', 'Create deliverables'); create.addEventListener('click', function () { runCreate(create); }); var cancel = el('button', 'btn btn-secondary', 'Close'); cancel.addEventListener('click', close); foot.appendChild(create); foot.appendChild(cancel); box.appendChild(foot); overlay.appendChild(box); document.body.appendChild(overlay); table = window.app.modules.seltable.create({ container: host, extraTitle: '', 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' }, ], }); table.render(); } 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 picked = {}; sel.forEach(function (i) { picked[i] = true; }); var deliverables = dedupe(files.filter(function (f) { return picked[f.id]; }), identityFields()); if (!deliverables.length) { T('None of the selected files split into deliverable fields.', 'warning'); return; } if (!confirm('Create ' + deliverables.length + ' deliverable(s) in the project MDL?\n\nOne .yaml per tracking number, in archive//mdl/. Already-present ones are skipped.')) return; btn.disabled = true; var s = { created: 0, skipped: 0, errors: 0 }; for (var i = 0; i < deliverables.length; i++) { setStatus('Creating ' + (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'); } } btn.disabled = false; setStatus(s.created + ' created, ' + s.skipped + ' already there' + (s.errors ? (', ' + s.errors + ' failed') : '') + '.'); T('MDL: ' + s.created + ' created, ' + s.skipped + ' already there' + (s.errors ? (', ' + s.errors + ' failed') : '') + '. Reload to see them.', s.errors ? 'warning' : 'success'); } // Show the toolbar button only on the project MDL rollup (addable:false + // an mdl path), over http, gated on create permission. Called from main.js // init once the context is known. function setup(ctx) { var btn = document.getElementById('table-add-from-archive'); if (!btn) return; var onHttp = location.protocol === 'http:' || location.protocol === 'https:'; var isMdlRollup = ctx && ctx.addable === false && /\/mdl\/(table\.html)?$/.test(location.pathname || ''); if (!(onHttp && isMdlRollup)) return; btn.hidden = false; btn.addEventListener('click', open); if (window.zddc && window.zddc.cap) { window.zddc.cap.at(archiveBaseUrl().replace(location.origin, '')).then(function (view) { var verbs = (view && view.path_verbs) || ''; if (verbs.indexOf('c') === -1) { btn.classList.add('is-disabled'); btn.title = "You don't have create access in this project's archive."; } }); } } app.modules.mdlFromArchive = { setup: setup, open: open, // test seams identityFields: identityFields, deliverableFromFile: deliverableFromFile, dedupe: dedupe, walkArchive: walkArchive, instantiateOne: instantiateOne, }; })(window.tablesApp);