Two layers shipped together since the second builds on the first. LAYER 1 — reviewing/ + Plan Review scaffolding - reviewing/ is now a real folder under each project, populated by the Plan Review composite endpoint. The old reviewing/ virtual aggregator handler is retired. - POST /<project>/archive/<party>/received/<tracking>/ with X-ZDDC-Op: plan-review scaffolds physical workflow folders under reviewing_root and staging_root, each carrying .zddc.received_path pointing back at the canonical submittal. Idempotent re-runs match by received_path and re-converge the ACL. - Virtual received window: when listing or writing under <workflow>/received/, the server resolves through the canonical archive/<party>/received/<tracking>/ via the workflow's .zddc.received_path. Writes get rewritten to <workflow>/<base>+C<n><suffix> so review comments land in the workflow folder and never touch the WORM archive. - Cascade defaults declare on_plan_review per project so the reviewing_root and staging_root are configurable. LAYER 2 — browse context-menu workflows - Accept Transmittal: right-click a transmittal folder in archive/<party>/incoming/ → validates ZDDC folder + filename conformance, atomic-renames the folder to archive/<party>/received/<tracking>/ (WORM zone), and optionally chains into Plan Review in the same composite request. Re-acceptance with a different revision merges file-by-file; WORM forbids overwrite of an existing filename. - Stage / Unstage: right-click files in working/<…>/ → "Stage to…" with picker of existing staging transmittal folders + inline "New transmittal folder…" create; right-click files in staging/<…>/ → "Unstage to working/" defaulting to the user's working/<email>/ home. Reuses the file-API move primitive. - Create Transmittal folder: right-click the staging/ pane → prompts for a ZDDC-conforming folder name with live validation; mkdir, then navigate to the new folder URL where the transmittal tool serves the editor. - Supporting infrastructure: new CanonicalFolderAt cascade lookup + X-ZDDC-Canonical-Folder response header so the browse SPA can scope-gate menu items without re-implementing the cascade client-side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
6.7 KiB
JavaScript
146 lines
6.7 KiB
JavaScript
// create-transmittal.js — folder-creation plumbing for outgoing
|
|
// transmittals.
|
|
//
|
|
// Surfaced by events.js as a pane-menu item (right-click empty space)
|
|
// when state.scopeCanonicalFolder == 'staging'. The modal prompts for
|
|
// a ZDDC-conforming folder name (date_tracking (purpose) - subject)
|
|
// with live validation via zddc.parseFolder, then POSTs X-ZDDC-Op:
|
|
// mkdir. On success the client navigates to the new folder URL — the
|
|
// staging/ cascade serves the transmittal tool there, where the user
|
|
// builds the manifest, adds files, and publishes.
|
|
//
|
|
// No manifest assembly happens here. This is plumbing.
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
function status(msg, level) {
|
|
var t = window.zddc && window.zddc.toast;
|
|
if (t) t(msg, level || 'info');
|
|
}
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, function (c) {
|
|
return ({ '&':'&','<':'<','>':'>','"':'"',"'":''' })[c];
|
|
});
|
|
}
|
|
function isoDateToday() {
|
|
var d = new Date();
|
|
return d.getFullYear()
|
|
+ '-' + ('0' + (d.getMonth() + 1)).slice(-2)
|
|
+ '-' + ('0' + d.getDate()).slice(-2);
|
|
}
|
|
|
|
function openForm() {
|
|
return new Promise(function (resolve, reject) {
|
|
var overlay = document.createElement('div');
|
|
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:9000;';
|
|
var box = document.createElement('div');
|
|
box.style.cssText = 'background:var(--bg,#fff);color:var(--fg,#111);padding:1.25rem 1.5rem;border-radius:6px;min-width:28rem;max-width:36rem;box-shadow:0 4px 20px rgba(0,0,0,0.25);';
|
|
box.innerHTML =
|
|
'<h2 style="margin:0 0 0.5rem 0;font-size:1.1rem;">Create Transmittal folder</h2>' +
|
|
'<p style="margin:0 0 0.6rem 0;font-size:0.85rem;color:#666;">' +
|
|
"After it's created, the transmittal tool opens here so you can build the manifest — " +
|
|
'add rows from the MDL, choose revisions, and associate files.' +
|
|
'</p>' +
|
|
'<label for="ct-name" style="font-size:0.9rem;">Folder name (ZDDC convention)</label>' +
|
|
'<input id="ct-name" type="text" style="width:100%;padding:0.3rem;font-family:var(--code,monospace);" ' +
|
|
'placeholder="YYYY-MM-DD_TRACKING (PURPOSE) - SUBJECT" value="' + escapeHtml(isoDateToday() + '_') + '">' +
|
|
'<div id="ct-feedback" style="font-size:0.8rem;color:#888;margin-top:0.2rem;min-height:1.1em;"></div>' +
|
|
'<div style="display:flex;justify-content:flex-end;gap:0.5rem;margin-top:1rem;">' +
|
|
'<button type="button" id="ct-cancel">Cancel</button>' +
|
|
'<button type="button" id="ct-submit" class="btn-primary" disabled>Create</button>' +
|
|
'</div>';
|
|
overlay.appendChild(box);
|
|
document.body.appendChild(overlay);
|
|
|
|
var input = box.querySelector('#ct-name');
|
|
var submit = box.querySelector('#ct-submit');
|
|
var feedback = box.querySelector('#ct-feedback');
|
|
function revalidate() {
|
|
var v = input.value.trim();
|
|
if (!v) {
|
|
feedback.textContent = '';
|
|
submit.disabled = true;
|
|
return;
|
|
}
|
|
var parsed = window.zddc.parseFolder(v);
|
|
if (parsed && parsed.valid) {
|
|
feedback.style.color = '#2a8';
|
|
feedback.textContent = '✓ tracking=' + parsed.trackingNumber +
|
|
', status=' + parsed.status + ', title=' + parsed.title;
|
|
submit.disabled = false;
|
|
} else {
|
|
feedback.style.color = '#c33';
|
|
feedback.textContent = '✗ does not match YYYY-MM-DD_TRACKING (PURPOSE) - SUBJECT';
|
|
submit.disabled = true;
|
|
}
|
|
}
|
|
input.addEventListener('input', revalidate);
|
|
revalidate();
|
|
|
|
function close() { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); }
|
|
box.querySelector('#ct-cancel').addEventListener('click', function () {
|
|
close(); reject(new Error('cancelled'));
|
|
});
|
|
overlay.addEventListener('click', function (e) {
|
|
if (e.target === overlay) { close(); reject(new Error('cancelled')); }
|
|
});
|
|
document.addEventListener('keydown', function escHandler(e) {
|
|
if (e.key === 'Escape') {
|
|
document.removeEventListener('keydown', escHandler);
|
|
close(); reject(new Error('cancelled'));
|
|
}
|
|
});
|
|
submit.addEventListener('click', function () {
|
|
var v = input.value.trim();
|
|
var parsed = window.zddc.parseFolder(v);
|
|
if (!parsed || !parsed.valid) {
|
|
status('Folder name must conform to ZDDC convention.', 'error');
|
|
return;
|
|
}
|
|
close(); resolve({ folderName: v });
|
|
});
|
|
|
|
// Position cursor after the date prefix.
|
|
setTimeout(function () {
|
|
input.focus();
|
|
input.setSelectionRange(input.value.length, input.value.length);
|
|
}, 0);
|
|
});
|
|
}
|
|
|
|
async function invoke() {
|
|
if (window.app.state.scopeCanonicalFolder !== 'staging') {
|
|
status('Create Transmittal folder is only available inside staging/.', 'error');
|
|
return;
|
|
}
|
|
var stagingUrl = window.app.state.currentPath || '/';
|
|
if (!stagingUrl.endsWith('/')) stagingUrl += '/';
|
|
|
|
var choice;
|
|
try { choice = await openForm(); } catch (_e) { return; }
|
|
var newUrl = stagingUrl + encodeURIComponent(choice.folderName) + '/';
|
|
|
|
var resp;
|
|
try {
|
|
resp = await fetch(newUrl, {
|
|
method: 'POST',
|
|
headers: { 'X-ZDDC-Op': 'mkdir' },
|
|
credentials: 'same-origin'
|
|
});
|
|
} catch (e) {
|
|
status('Create failed: ' + (e && e.message ? e.message : e), 'error');
|
|
return;
|
|
}
|
|
if (!resp.ok) {
|
|
var text = ''; try { text = await resp.text(); } catch (_e) {}
|
|
status('Create failed (' + resp.status + '): ' + text, 'error');
|
|
return;
|
|
}
|
|
status('Created ' + choice.folderName + ' — opening transmittal tool…', 'success');
|
|
// Navigate to the new folder (no-slash form → default_tool: transmittal).
|
|
window.location.href = stagingUrl + encodeURIComponent(choice.folderName);
|
|
}
|
|
|
|
window.app.modules.createTransmittal = { invoke: invoke };
|
|
})();
|