chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 3s
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 3s
This commit is contained in:
parent
93ed0d361f
commit
8e10e5e5e6
7 changed files with 1137 additions and 45 deletions
|
|
@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<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-11 14:40:24 · bc762a7</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||
|
|
|
|||
|
|
@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<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-11 14:40:25 · bc762a7</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3</span></span>
|
||||
</div>
|
||||
<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>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1778,7 +1778,7 @@ body {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-11 14:40:24 · bc762a7</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
|
|||
|
|
@ -2770,7 +2770,7 @@ dialog.modal--narrow {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<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-11 14:40:24 · bc762a7</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3</span></span>
|
||||
</div>
|
||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||
<!-- Publish split-button (Transmittal-specific primary action;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||
archive=v0.0.27-beta · 2026-06-11 14:40:24 · bc762a7
|
||||
transmittal=v0.0.27-beta · 2026-06-11 14:40:24 · bc762a7
|
||||
classifier=v0.0.27-beta · 2026-06-11 14:40:24 · bc762a7
|
||||
landing=v0.0.27-beta · 2026-06-11 14:40:24 · bc762a7
|
||||
form=v0.0.27-beta · 2026-06-11 14:40:24 · bc762a7
|
||||
tables=v0.0.27-beta · 2026-06-11 14:40:25 · bc762a7
|
||||
browse=v0.0.27-beta · 2026-06-11 14:40:25 · bc762a7
|
||||
archive=v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3
|
||||
transmittal=v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3
|
||||
classifier=v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3
|
||||
landing=v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3
|
||||
form=v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3
|
||||
tables=v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3
|
||||
browse=v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3
|
||||
|
|
|
|||
|
|
@ -1245,6 +1245,53 @@ body.is-elevated::after {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Shared selectable + autofilter table (seltable) + its hosting overlay ───
|
||||
Used by the tables tool's "Add from archive". The classifier carries an
|
||||
equivalent copy inline in its layout.css for the catalog. */
|
||||
.seltable { display: flex; flex-direction: column; min-height: 0; height: 100%; }
|
||||
.seltable__bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
|
||||
.seltable__count { color: var(--text-muted); font-size: 0.78rem; white-space: nowrap; }
|
||||
.seltable__scroll { flex: 1; min-height: 0; overflow: auto; }
|
||||
.seltable__table { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.82rem; }
|
||||
.seltable__table th, .seltable__table td { border-bottom: 1px solid var(--border); padding: 0.25rem 0.5rem; text-align: left; white-space: nowrap; }
|
||||
.seltable__table thead th {
|
||||
position: sticky; top: 0; z-index: 2; background: var(--bg-secondary, var(--bg));
|
||||
color: var(--text-muted); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
||||
}
|
||||
.seltable__table thead tr.seltable__filters th { top: 1.55rem; padding: 0.15rem 0.35rem; }
|
||||
.seltable__colfilter {
|
||||
width: 100%; min-width: 5rem; padding: 0.15rem 0.35rem;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
background: var(--bg); color: var(--text); font-size: 0.74rem; font-weight: 400; text-transform: none; letter-spacing: 0;
|
||||
}
|
||||
.seltable__row { cursor: pointer; user-select: none; }
|
||||
.seltable__row:hover { background: var(--bg-hover); }
|
||||
.seltable__row.is-selected { background: var(--primary-light, rgba(37,99,235,0.12)); }
|
||||
.seltable__row.is-selected:hover { background: var(--primary-light, rgba(37,99,235,0.18)); }
|
||||
.seltable__row.drop-hover { outline: 2px solid var(--primary); outline-offset: -2px; }
|
||||
|
||||
/* ── "Add deliverables from archive" overlay (project MDL rollup) ─────────── */
|
||||
.mdlarch-overlay {
|
||||
position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex; align-items: center; justify-content: center; padding: 1.5rem;
|
||||
}
|
||||
.mdlarch-overlay__box {
|
||||
display: flex; flex-direction: column; min-height: 0;
|
||||
width: min(960px, 95vw); height: min(80vh, 760px);
|
||||
background: var(--bg); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.mdlarch-overlay__head { display: flex; align-items: center; gap: 0.75rem; padding: 0.85rem 1.1rem; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
|
||||
.mdlarch-overlay__head h2 { margin: 0; font-size: 1.05rem; flex: 1; }
|
||||
.mdlarch-overlay__close { border: none; background: none; color: var(--text-muted); font-size: 1.4rem; line-height: 1; cursor: pointer; padding: 0 0.25rem; }
|
||||
.mdlarch-overlay__close:hover { color: var(--text); }
|
||||
.mdlarch-overlay__status { padding: 0.5rem 1.1rem; color: var(--text-muted); font-size: 0.82rem; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
|
||||
.mdlarch-overlay__table { flex: 1; min-height: 0; display: flex; }
|
||||
.mdlarch-overlay__table .seltable { height: 100%; flex: 1; }
|
||||
.mdlarch-overlay__foot { display: flex; justify-content: flex-end; gap: 0.6rem; padding: 0.75rem 1.1rem; border-top: 1px solid var(--border); flex: 0 0 auto; }
|
||||
|
||||
/* tables/ — directory-of-YAML table view. Reuses tokens from shared/base.css. */
|
||||
|
||||
.table-main {
|
||||
|
|
@ -1722,7 +1769,7 @@ body.is-elevated::after {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<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-11 14:40:25 · bc762a7</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-12 02:15:36 · 93ed0d3</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
@ -1743,6 +1790,7 @@ body.is-elevated::after {
|
|||
<div class="table-toolbar__right">
|
||||
<button type="button" id="table-save" class="btn btn-primary btn-sm" hidden>Save</button>
|
||||
<button type="button" id="table-export-csv" class="btn btn-secondary btn-sm" hidden>Export CSV</button>
|
||||
<button type="button" id="table-add-from-archive" class="btn btn-secondary btn-sm" hidden title="Register deliverables from existing archive files (project MDL rollup)">+ From archive</button>
|
||||
<a id="table-add-row" class="btn btn-primary btn-sm" hidden>+ Add row</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3996,6 +4044,185 @@ body.is-elevated::after {
|
|||
window.zddc.menu = { open: open, close: close };
|
||||
})();
|
||||
|
||||
/**
|
||||
* ZDDC — shared selectable + autofilter table (used by the classifier catalog
|
||||
* and the tables tool's "Add from archive").
|
||||
*
|
||||
* A flat table with PER-COLUMN autofilters (one input per column, AND-combined,
|
||||
* each an AND of space-separated terms) plus an optional programmatic global
|
||||
* filter, and powerful selection for building complex sets quickly:
|
||||
* click replace selection + set anchor
|
||||
* ctrl/cmd-click toggle one row
|
||||
* shift-click range from the anchor (replaces the selection)
|
||||
* ctrl-shift-click ADD the anchor→row range to the existing selection
|
||||
* ctrl/cmd-Enter fire onActivate(selectedIds) — a bulk action
|
||||
* Esc clear
|
||||
* Ranges run over the CURRENTLY FILTERED order. Selection is keyed by a stable
|
||||
* rowId so it survives filtering and re-render.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
if (!window.app) window.app = {};
|
||||
if (!window.app.modules) window.app.modules = {};
|
||||
|
||||
function terms(q) { return String(q == null ? '' : q).trim().toLowerCase().split(/\s+/).filter(Boolean); }
|
||||
function hit(text, ts) {
|
||||
var t = String(text == null ? '' : text).toLowerCase();
|
||||
for (var i = 0; i < ts.length; i++) { if (t.indexOf(ts[i]) === -1) return false; }
|
||||
return true;
|
||||
}
|
||||
function elt(tag, cls, text) {
|
||||
var e = document.createElement(tag);
|
||||
if (cls) e.className = cls;
|
||||
if (text != null) e.textContent = text;
|
||||
return e;
|
||||
}
|
||||
|
||||
function create(opts) {
|
||||
var container = opts.container;
|
||||
var columns = opts.columns || [];
|
||||
var rowId = opts.rowId || function (r) { return r.id; };
|
||||
var getRows = (typeof opts.rows === 'function') ? opts.rows : function () { return opts.rows || []; };
|
||||
var selected = Object.create(null); // id -> true
|
||||
var anchorId = null;
|
||||
var globalTerms = []; // programmatic global filter (tests/reveal)
|
||||
var colFilters = Object.create(null); // colKey -> terms[] (the per-column autofilters)
|
||||
|
||||
function rows() { return getRows() || []; }
|
||||
function colByKey(k) { for (var i = 0; i < columns.length; i++) { if (columns[i].key === k) return columns[i]; } return null; }
|
||||
function colVal(col, row) { return col.get ? col.get(row) : (row[col.key] == null ? '' : row[col.key]); }
|
||||
function rowBlob(row) { var s = ''; for (var i = 0; i < columns.length; i++) { s += colVal(columns[i], row) + ' '; } return s; }
|
||||
function rowMatches(row) {
|
||||
if (globalTerms.length && !hit(rowBlob(row), globalTerms)) return false;
|
||||
for (var k in colFilters) {
|
||||
var col = colByKey(k);
|
||||
if (col && !hit(colVal(col, row), colFilters[k])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function filtered() { return rows().filter(rowMatches); }
|
||||
|
||||
function getSelection() { return Object.keys(selected); }
|
||||
function getFilteredRows() { return filtered(); }
|
||||
function fireSel() { if (opts.onSelectionChange) opts.onSelectionChange(getSelection()); }
|
||||
function setFilter(q) { globalTerms = terms(q); renderBody(); }
|
||||
function setColFilter(colKey, q) { var t = terms(q); if (t.length) colFilters[colKey] = t; else delete colFilters[colKey]; renderBody(); }
|
||||
function selectAllFiltered() { filtered().forEach(function (r) { selected[rowId(r)] = true; }); anchorId = null; renderBody(); fireSel(); }
|
||||
function clearSel() { selected = Object.create(null); anchorId = null; renderBody(); fireSel(); }
|
||||
|
||||
function onRowClick(e, row, fr) {
|
||||
var ids = fr.map(rowId), id = rowId(row), idx = ids.indexOf(id), aIdx;
|
||||
if (e.shiftKey && anchorId != null && (aIdx = ids.indexOf(anchorId)) >= 0) {
|
||||
if (!(e.ctrlKey || e.metaKey)) selected = Object.create(null); // shift replaces; ctrl-shift adds
|
||||
var lo = Math.min(aIdx, idx), hi = Math.max(aIdx, idx);
|
||||
for (var i = lo; i <= hi; i++) selected[ids[i]] = true;
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
if (selected[id]) delete selected[id]; else selected[id] = true;
|
||||
anchorId = id;
|
||||
} else {
|
||||
selected = Object.create(null); selected[id] = true; anchorId = id;
|
||||
}
|
||||
renderBody(); fireSel();
|
||||
}
|
||||
|
||||
var bodyEl = null, countEl = null;
|
||||
function render() {
|
||||
container.textContent = '';
|
||||
container.classList.add('seltable');
|
||||
var bar = elt('div', 'seltable__bar');
|
||||
var allBtn = elt('button', 'btn btn-sm btn-secondary', 'Select filtered');
|
||||
allBtn.addEventListener('click', selectAllFiltered);
|
||||
var clrBtn = elt('button', 'btn btn-sm btn-secondary', 'Clear');
|
||||
clrBtn.addEventListener('click', clearSel);
|
||||
countEl = elt('span', 'seltable__count');
|
||||
bar.appendChild(allBtn); bar.appendChild(clrBtn); bar.appendChild(countEl);
|
||||
container.appendChild(bar);
|
||||
|
||||
var scroll = elt('div', 'seltable__scroll');
|
||||
var table = elt('table', 'seltable__table');
|
||||
var thead = elt('thead'), htr = elt('tr');
|
||||
columns.forEach(function (c) { htr.appendChild(elt('th', c.cls || null, c.title || c.key)); });
|
||||
if (opts.rowExtra) htr.appendChild(elt('th', 'seltable__extrah', opts.extraTitle || ''));
|
||||
thead.appendChild(htr);
|
||||
// Per-column autofilter row.
|
||||
var ftr = elt('tr', 'seltable__filters');
|
||||
columns.forEach(function (c) {
|
||||
var th = elt('th');
|
||||
if (c.filterable !== false) {
|
||||
var inp = elt('input', 'seltable__colfilter'); inp.type = 'search'; inp.placeholder = 'filter…'; inp.spellcheck = false;
|
||||
inp.setAttribute('data-no-select', '');
|
||||
inp.addEventListener('input', function () { setColFilter(c.key, this.value); });
|
||||
th.appendChild(inp);
|
||||
}
|
||||
ftr.appendChild(th);
|
||||
});
|
||||
if (opts.rowExtra) ftr.appendChild(elt('th'));
|
||||
thead.appendChild(ftr);
|
||||
table.appendChild(thead);
|
||||
bodyEl = elt('tbody'); table.appendChild(bodyEl);
|
||||
scroll.appendChild(table); container.appendChild(scroll);
|
||||
|
||||
container.tabIndex = 0;
|
||||
container.addEventListener('keydown', function (e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (opts.onActivate) opts.onActivate(getSelection()); }
|
||||
else if (e.key === 'Escape') { clearSel(); }
|
||||
});
|
||||
renderBody();
|
||||
}
|
||||
function renderBody() {
|
||||
if (!bodyEl) return;
|
||||
var fr = filtered();
|
||||
bodyEl.textContent = '';
|
||||
fr.forEach(function (row) {
|
||||
var id = rowId(row);
|
||||
var tr = elt('tr', 'seltable__row' + (selected[id] ? ' is-selected' : ''));
|
||||
tr.dataset.id = id;
|
||||
tr.addEventListener('click', function (e) {
|
||||
if (e.target.closest('input,button,select,a,[data-no-select]')) return;
|
||||
onRowClick(e, row, fr);
|
||||
});
|
||||
if (opts.onRowDrop) {
|
||||
tr.addEventListener('dragover', function (e) {
|
||||
if (window.app.modules.dnd && window.app.modules.dnd.active()) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; tr.classList.add('drop-hover'); }
|
||||
});
|
||||
tr.addEventListener('dragleave', function () { tr.classList.remove('drop-hover'); });
|
||||
tr.addEventListener('drop', function (e) {
|
||||
tr.classList.remove('drop-hover');
|
||||
e.preventDefault();
|
||||
var keys = window.app.modules.dnd ? window.app.modules.dnd.getDrag() : [];
|
||||
if (window.app.modules.dnd) window.app.modules.dnd.clearDrag();
|
||||
if (keys.length) opts.onRowDrop(id, keys);
|
||||
});
|
||||
}
|
||||
columns.forEach(function (c) {
|
||||
var td = elt('td', c.cls || null);
|
||||
if (c.render) c.render(row, td); else td.textContent = colVal(c, row);
|
||||
tr.appendChild(td);
|
||||
});
|
||||
if (opts.rowExtra) { var ex = elt('td', 'seltable__extra'); opts.rowExtra(row, ex); tr.appendChild(ex); }
|
||||
bodyEl.appendChild(tr);
|
||||
});
|
||||
if (countEl) {
|
||||
var nSel = getSelection().length;
|
||||
countEl.textContent = fr.length + ' shown' + (nSel ? ' · ' + nSel + ' selected' : '');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
render: render, renderBody: renderBody,
|
||||
getSelection: getSelection, getFilteredRows: getFilteredRows,
|
||||
setFilter: setFilter, setColFilter: setColFilter, selectAllFiltered: selectAllFiltered, clear: clearSel,
|
||||
clickRow: function (id, mods) {
|
||||
var fr = filtered();
|
||||
var row = fr.filter(function (r) { return String(rowId(r)) === String(id); })[0];
|
||||
if (row) onRowClick(Object.assign({ shiftKey: false, ctrlKey: false, metaKey: false }, mods || {}), row, fr);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
window.app.modules.seltable = { create: create };
|
||||
})();
|
||||
|
||||
// mode.js — picks table-mode vs form-mode at boot time and unhides the
|
||||
// matching container. Both apps (tablesApp, formApp) ship in the same
|
||||
// bundle but each only paints when its container is visible.
|
||||
|
|
@ -7571,6 +7798,191 @@ body.is-elevated::after {
|
|||
}
|
||||
})(window.tablesApp = window.tablesApp || {});
|
||||
|
||||
// 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 (<project>/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/<originator>/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\/.*$/, '/'); // <project>/
|
||||
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/<originator>/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);
|
||||
|
||||
(function (app) {
|
||||
'use strict';
|
||||
|
||||
|
|
@ -7740,6 +8152,11 @@ body.is-elevated::after {
|
|||
}
|
||||
}
|
||||
|
||||
// "Add from archive" — shown only on the project MDL rollup (own gating).
|
||||
if (app.modules.mdlFromArchive && app.modules.mdlFromArchive.setup) {
|
||||
app.modules.mdlFromArchive.setup(ctx);
|
||||
}
|
||||
|
||||
const columns = Array.isArray(ctx.columns) ? ctx.columns : [];
|
||||
const allRows = Array.isArray(ctx.rows) ? ctx.rows : [];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue