From 8ee7f1c460b59d21ef61cc8a7cc05a0e91fc3deb Mon Sep 17 00:00:00 2001 From: ZDDC Date: Tue, 16 Jun 2026 14:19:12 -0500 Subject: [PATCH] chore(embedded): cut v0.0.27-beta --- zddc/internal/apps/embedded/archive.html | 2 +- zddc/internal/apps/embedded/browse.html | 2 +- zddc/internal/apps/embedded/classifier.html | 732 +++++++++++-------- zddc/internal/apps/embedded/index.html | 2 +- zddc/internal/apps/embedded/transmittal.html | 2 +- zddc/internal/apps/embedded/versions.txt | 14 +- zddc/internal/handler/tables.html | 2 +- 7 files changed, 433 insertions(+), 323 deletions(-) diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 9bca4f7..aa4ee08 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a + v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index 12b24e3..9694dd0 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
ZDDC Browse - v0.0.27-beta · 2026-06-16 13:52:24 · 1bb5d1a + v0.0.27-beta · 2026-06-16 19:19:06 · 054cf2d
diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index 9018797..525a2e8 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -1729,6 +1729,10 @@ body.is-elevated::after { .file-item:hover { background: var(--bg-hover); } .file-item:active { cursor: grabbing; } .file-item.match-highlight { background: var(--primary-light); outline: 1px solid var(--primary); } +/* Multi-selected source files (ctrl/shift-click) — these drag together and fill a + contiguous block of grid rows. */ +.file-item.selected { background: var(--primary-light, rgba(37,99,235,0.12)); outline: 1px solid var(--primary); outline-offset: -1px; } +.file-item.selected:hover { background: var(--primary-light, rgba(37,99,235,0.18)); } .folder-item[draggable="true"] { cursor: grab; } .file-icon { color: var(--text-muted); } .file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -1980,9 +1984,15 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o .tg-orig__link { color: var(--text-muted); white-space: nowrap; text-decoration: none; cursor: pointer; } .tg-orig__link:hover { text-decoration: underline; } .tg-status, .tg-x { text-align: center; } +/* Placeholder rows (a list item with no file yet) — a hollow "wanted" marker and + a muted expected-name hint that doubles as the drop target. */ +.tg-wanted { color: var(--text-muted); } +.tg-expected { color: var(--text-muted); font-style: italic; } .tg-x__btn { opacity: 0.5; } .seltable__row:hover .tg-x__btn { opacity: 1; } .tg-drop-hover { outline: 2px dashed var(--primary); outline-offset: -3px; background: var(--primary-light); } +/* The exact placeholder rows a multi-file drop will fill (the drag indicator). */ +.seltable__row.tg-fill-target { background: var(--primary-light, rgba(37,99,235,0.18)); outline: 2px solid var(--primary); outline-offset: -2px; } /* "Columns ▾" chooser menu */ .col-chooser { @@ -2478,14 +2488,10 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
ZDDC Classifier - v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a + v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
-
- - -
@@ -2548,81 +2554,15 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
- -
-
-
-

Files

-
- 0 files - 0 modified - -
-
-
- - - | - - - | - -
-
-
- - - - - - - - - - - - - - - - - -
#Original Filename - - Ext - - New Filename - - Tracking - - Rev - - Status - - Title - -
-
-
- - +
-

Each file needs a tracking number (revision + status + title) and a transmittal folder. Name it — build one under By tracking number, or drag onto a row under From a list (loaded from the archive/MDL or pasted from Excel) — then route it under By transmittal.

+

Give each file a tracking number (revision + status + title) under By tracking number — that alone is a Rename in place. Route it into a transmittal folder under By transmittal to Copy it into the archive.

-
- - -
- -
- -
+ +
@@ -2631,15 +2571,19 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o - -
+ + + + + - Drag files in, then type each one’s tracking number, revision (e.g. “A (IFR)”), and title. A file that’s already ZDDC-named fills in automatically. Columns are hideable + resizable. + Drag files in (ctrl-shift-click the left tree to multi-select; drop on a row to fill a contiguous block), or “Add filtered files”. Type the tracking number, revision (“A (IFR)”) and title — already-ZDDC-named files fill in. “Load…” / “Paste rows…” add rows to drop files onto. +
@@ -2648,28 +2592,14 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o - -
@@ -2699,26 +2629,16 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
- +
-

① Classify & copy recommended · non-destructive

-

Build a tidy copy of a project in a separate output folder. Your source files are only ever read, never renamed or moved.

+

Classify, then Rename or Copy

+

Give each file a ZDDC name in the By tracking number grid. A tracking number alone is a rename; add a transmittal folder and it can be copied into the archive.

  1. New workspace → pick a folder. It scans once and saves to this browser, so you can close the tab and pick up later.
  2. -
  3. Preview a file (single-click it in the left tree) to see what it actually is.
  4. -
  5. Drag it onto the right pane — onto a tracking-number folder (the folder path becomes the number, the leaf is the revision, e.g. A (IFR)), and onto a transmittal (party + date + TRN/SUB + sequence).
  6. -
  7. Copy when ready → choose an output directory; renamed copies are written as <party>/<transmittal>/<name>, with duplicates detected.
  8. -
-
-
-

② Rename in place edits your files

-

A quick spreadsheet for a folder you own: fill in tracking number, revision, status and title, and rename the files on disk.

-
    -
  1. Click Use Local Directory (top bar) to open a folder.
  2. -
  3. Switch the toggle to Rename in place.
  4. -
  5. Edit cells (or paste columns from Excel); names already in ZDDC format are parsed automatically and validated as you type.
  6. -
  7. Save All renames the files where they sit.
  8. +
  9. Add files to the grid — drag them from the left tree (ctrl-shift-click to multi-select and fill a block of rows), use ⊕ Add filtered files, or ⊞ Load… / ⎘ Paste rows… a list of expected tracking numbers and drop the matching files on. Already-ZDDC-named files fill in automatically.
  10. +
  11. Type each file's tracking number, revision (e.g. A (IFR)) and title.
  12. +
  13. Rename… (By tracking number) renames the named files in place on disk — destructive, no backup. Or place them into a transmittal under By transmittal and Copy… them into the archive (source untouched, resumable, verified).
@@ -6099,22 +6019,19 @@ X.B(E,Y);return E}return J}()) * onto target trees, copy renamed copies out). The source tree (left) stays * in both modes; only the right pane swaps. */ - function setMode(mode) { - const classify = mode === 'classify'; - app.dom.modeRenameBtn.classList.toggle('active', !classify); - app.dom.modeClassifyBtn.classList.toggle('active', classify); - if (app.dom.spreadsheetPane) app.dom.spreadsheetPane.hidden = classify; - if (app.dom.targetPane) app.dom.targetPane.hidden = !classify; - // Mode-specific source-tree filters: "Hide Compliant" is for the rename - // grid; "Hide Assigned" is for the classify workflow. - if (app.dom.hideCompliantLabel) app.dom.hideCompliantLabel.hidden = classify; - if (app.dom.classifyFilters) app.dom.classifyFilters.hidden = !classify; - app.modules.classify.setEnabled(classify); - if (classify && app.modules.targetTree) { + // There is only one surface now (the classify grid + transmittal tree); the + // old Rename-in-place spreadsheet was folded into the By-tracking grid's + // "Rename…" action. setMode is kept as a no-arg enabler for back-compat with + // the workspace/open flows that call it. + function setMode() { + if (app.dom.targetPane) app.dom.targetPane.hidden = false; + if (app.dom.classifyFilters) app.dom.classifyFilters.hidden = false; + app.modules.classify.setEnabled(true); + if (app.modules.targetTree) { app.modules.targetTree.init(); app.modules.targetTree.render(); } - // Re-render the source tree so its per-file markers appear/disappear. + // Re-render the source tree so its per-file markers appear. if (app.modules.tree) app.modules.tree.render(); } @@ -6245,18 +6162,9 @@ X.B(E,Y);return E}return J}()) // Drag and drop on welcome screen setupWelcomeDragDrop(); - // Bulk actions - app.dom.saveAllBtn.addEventListener('click', handleSaveAll); - app.dom.cancelAllBtn.addEventListener('click', handleCancelAll); - - // Export hashes - app.dom.exportHashesBtn.addEventListener('click', handleExportHashes); - - // SHA256 toggle - app.dom.sha256Checkbox.addEventListener('change', handleSha256Toggle); - - // Hide compliant toggle - app.dom.hideCompliantCheckbox.addEventListener('change', handleHideCompliantToggle); + // (The old Rename-in-place spreadsheet — Save All / Cancel All / SHA256 / + // Export hashes — was removed; its rename is now the By-tracking "Rename…".) + if (app.dom.hideCompliantCheckbox) app.dom.hideCompliantCheckbox.addEventListener('change', handleHideCompliantToggle); // Classify-mode source-tree filters: show/hide unassigned, assigned, excluded. function pushClassifyFilters() { @@ -6281,9 +6189,6 @@ X.B(E,Y);return E}return J}()) // Collapse tree button app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree); - // Workflow mode switch - if (app.dom.modeRenameBtn) app.dom.modeRenameBtn.addEventListener('click', function () { setMode('rename'); }); - if (app.dom.modeClassifyBtn) app.dom.modeClassifyBtn.addEventListener('click', function () { setMode('classify'); }); if (app.dom.copyOutputBtn) app.dom.copyOutputBtn.addEventListener('click', function () { app.modules.copy.run(); }); if (app.dom.checkDuplicatesBtn) app.dom.checkDuplicatesBtn.addEventListener('click', function () { app.modules.copy.audit(); }); @@ -6449,12 +6354,7 @@ X.B(E,Y);return E}return J}()) showMainUI(); if (!shellInited) { shellInited = true; - app.modules.spreadsheet.init(); // Subscribe to store - app.modules.selection.init(); - app.modules.preview.init(); // After selection so it can listen for rowfocused - app.modules.resize.init(); - app.modules.filter.init(); - app.modules.sort.init(); + app.modules.preview.init(); // file preview (click a row / original-name link) app.modules.tree.setupKeyboardShortcuts(); if (app.modules.targetTree) app.modules.targetTree.init(); } @@ -6464,9 +6364,8 @@ X.B(E,Y);return E}return J}()) async function openDirectory(dirHandle) { app.rootHandle = dirHandle; enterAppShell(); - // Default to Classify & Copy (the primary workflow). The user can switch - // to "Rename in place" via the toggle for the spreadsheet. - setMode('classify'); + setMode(); // the single classify surface + // Now scan directory (this will trigger store updates and renders) await app.modules.scanner.scanDirectory(dirHandle); } @@ -6575,18 +6474,8 @@ X.B(E,Y);return E}return J}()) * Handle keyboard shortcuts */ function handleKeyDown(e) { - // Ctrl+S - Save All - if (e.ctrlKey && e.key === 's') { - e.preventDefault(); - if (!app.dom.saveAllBtn.disabled) { - handleSaveAll(); - } - } - - // Escape - Cancel editing - if (e.key === 'Escape') { - app.modules.spreadsheet.cancelEditing(); - } + // (Spreadsheet Ctrl+S / Escape handlers removed with the Rename-in-place + // pane. The By-tracking grid commits edits on change.) } /** @@ -6618,6 +6507,7 @@ X.B(E,Y);return E}return J}()) * Update stats display */ function updateStats() { + if (!app.dom.totalFiles) return; // spreadsheet pane removed — nothing to update const files = app.modules.store.getDisplayFiles(); const totalFiles = files.length; const modifiedFiles = files.filter(f => f.isDirty).length; @@ -7972,6 +7862,19 @@ X.B(E,Y);return E}return J}()) Object.keys(state.assignments).forEach(function (k) { if (state.assignments[k].trackingNodeId) set[k] = true; }); return Object.keys(set); } + // A file was renamed on disk to its canonical name — it's done, so drop it from + // the grid model entirely (under its OLD key): clear both placements, the + // workset, the assignment, and any worklist-row binding. The compliant file + // object itself stays in the scanned tree. No notify — the caller batches. + function forgetFile(key) { + var a = state.assignments[key], oldTrack = a ? a.trackingNodeId : null; + delete state.trackingWorkset[key]; + place([key], null, 'tracking'); + place([key], null, 'transmittal'); + delete state.assignments[key]; + state.worklist.forEach(function (r) { if (r.placed) delete r.placed[key]; if (r.bound) delete r.bound[key]; }); + if (oldTrack) pruneEmptyTrackingChain(oldTrack); + } // Re-materialize a file's tracking placement from a full identity. The caller // passes ALL three fields (current values for the ones it didn't edit), read // from deriveTarget — so this module needs no file objects. A blank revision @@ -8057,6 +7960,13 @@ X.B(E,Y);return E}return J}()) notify(); } function clearWorklist() { state.worklist = []; notify(); } // rows only — assignments survive + // Remove ONE worklist row (a placeholder's ✕). Any files it placed keep their + // assignments — only the scratch row goes away. + function removeWorklistRow(id) { + var before = state.worklist.length; + state.worklist = state.worklist.filter(function (r) { return r.id !== id; }); + if (state.worklist.length !== before) notify(); + } function getWorklist() { return state.worklist; } function getWorklistRow(id) { return state.worklist.filter(function (r) { return r.id === id; })[0] || null; } @@ -8441,8 +8351,9 @@ X.B(E,Y);return E}return J}()) getConfig: getConfig, setConfig: setConfig, getTrackingFields: getTrackingFields, // By-tracking grid addToTrackingGrid: addToTrackingGrid, removeFromTrackingGrid: removeFromTrackingGrid, - trackingGridKeys: trackingGridKeys, setFileIdentity: setFileIdentity, + trackingGridKeys: trackingGridKeys, setFileIdentity: setFileIdentity, forgetFile: forgetFile, setWorklist: setWorklist, appendWorklist: appendWorklist, clearWorklist: clearWorklist, + removeWorklistRow: removeWorklistRow, getWorklist: getWorklist, getWorklistRow: getWorklistRow, assignFromRow: assignFromRow, unassignRowFile: unassignRowFile, setRowTracking: setRowTracking, setRowTitle: setRowTitle, @@ -10679,11 +10590,12 @@ X.B(E,Y);return E}return J}()) item.className = 'file-item'; item.style.paddingLeft = `${level * 1.5}rem`; item.draggable = true; - item.title = 'Click to preview · drag onto a tracking folder or transmittal to assign'; + item.title = 'Click to preview · ctrl/shift-click to multi-select · drag onto the grid (a block of rows) or a transmittal'; const key = c.srcKeyForFile(file); item.dataset.key = key; const st = c.fileState(file); if (st === 'excluded') item.classList.add('excluded'); + if (selectedFileKeys[key]) item.classList.add('selected'); item.appendChild(stateDot(st)); @@ -10699,11 +10611,43 @@ X.B(E,Y);return E}return J}()) item.addEventListener('dragstart', function (e) { e.stopPropagation(); - window.app.modules.dnd.setDrag([key], e); + // Drag the whole multi-selection (in visible top-to-bottom order) when + // this file is part of it; otherwise just this one. + var keys = (selectedFileKeys[key] && fileSelectionCount() > 1) + ? visibleFileKeys().filter(function (k) { return selectedFileKeys[k]; }) + : [key]; + window.app.modules.dnd.setDrag(keys, e); }); return item; } + // ── source-file multi-selection (drives a multi-row drag onto the grid) ── + var selectedFileKeys = Object.create(null), fileAnchorKey = null; + function fileSelectionCount() { return Object.keys(selectedFileKeys).length; } + function visibleFileKeys() { + return Array.prototype.map.call(window.app.dom.folderTree.querySelectorAll('.file-item'), function (el) { return el.dataset.key; }); + } + function applyFileSelectionClasses() { + Array.prototype.forEach.call(window.app.dom.folderTree.querySelectorAll('.file-item'), function (el) { + el.classList.toggle('selected', !!selectedFileKeys[el.dataset.key]); + }); + } + // click = replace + anchor; ctrl/cmd = toggle; shift = range from anchor; + // ctrl-shift = add range. Ranges run over the visible (DOM) file order. + function selectFileClick(key, e) { + var keys = visibleFileKeys(), a, b; + if (e.shiftKey && fileAnchorKey != null && (a = keys.indexOf(fileAnchorKey)) >= 0 && (b = keys.indexOf(key)) >= 0) { + if (!(e.ctrlKey || e.metaKey)) selectedFileKeys = Object.create(null); + for (var i = Math.min(a, b); i <= Math.max(a, b); i++) selectedFileKeys[keys[i]] = true; + } else if (e.ctrlKey || e.metaKey) { + if (selectedFileKeys[key]) delete selectedFileKeys[key]; else selectedFileKeys[key] = true; + fileAnchorKey = key; + } else { + selectedFileKeys = Object.create(null); selectedFileKeys[key] = true; fileAnchorKey = key; + } + applyFileSelectionClasses(); + } + /** * Handle folder click with multi-select support */ @@ -11028,15 +10972,19 @@ X.B(E,Y);return E}return J}()) var ft = window.app.dom.folderTree; if (!ft) { classifyWired = false; return; } ft.addEventListener('contextmenu', onContextMenu); - // Single-click a source file → preview it (the "look at it, then assign" - // half of the workflow). Drag still assigns; right-click excludes. + // Click a source file → update the multi-selection (ctrl/shift) AND, on a + // plain click, preview it (the "look at it, then assign" half). Drag of a + // selected file drags the whole selection; right-click excludes. ft.addEventListener('click', function (e) { if (!classifyOn()) return; var fe = e.target.closest('.file-item'); if (!fe || !fe.dataset.key) return; - var file = findFileByKey(fe.dataset.key); - if (file && window.app.modules.preview && window.app.modules.preview.previewFile) { - window.app.modules.preview.previewFile(file); + selectFileClick(fe.dataset.key, e); + if (!e.ctrlKey && !e.metaKey && !e.shiftKey) { + var file = findFileByKey(fe.dataset.key); + if (file && window.app.modules.preview && window.app.modules.preview.previewFile) { + window.app.modules.preview.previewFile(file); + } } }); } @@ -11207,6 +11155,7 @@ X.B(E,Y);return E}return J}()) setShowFilters, setNameFilter, exportFilteredList, + filteredFiles: filteredFileObjects, _buildExportTsv: buildExportTsv }; })(); @@ -11370,10 +11319,7 @@ X.B(E,Y);return E}return J}()) var collapsed = {}; // nodeId -> true when collapsed (default expanded) var openForm = null; // { partyId, slot } when a bin form is open var initialized = false; - var currentTab = 'tracking'; // 'tracking' | 'worklist' | 'transmittal' — active tab - var worklistGrid = null; // the seltable controller for the "From a list" tab - var worklistPlaced = {}; // trackingNumber -> placed files (read by the Files cell) - var hideAssigned = false; // "Hide assigned" toggle in the From-a-list toolbar + var currentTab = 'tracking'; // 'tracking' | 'transmittal' — active tab var listScanned = false; // a Load has run this session (drives the "new" badge) function init() { @@ -11381,49 +11327,43 @@ X.B(E,Y);return E}return J}()) initialized = true; els = { trackingTab: document.getElementById('trackingTab'), - worklistTab: document.getElementById('worklistTab'), transmittalTab: document.getElementById('transmittalTab'), trackingPanel: document.getElementById('trackingPanel'), transmittalPanel: document.getElementById('transmittalPanel'), - worklistPanel: document.getElementById('worklistPanel'), trackingTree: document.getElementById('trackingTree'), transmittalTree: document.getElementById('transmittalTree'), - worklistTable: document.getElementById('worklistTable'), loadWorklistBtn: document.getElementById('loadWorklistBtn'), pasteRowsBtn: document.getElementById('pasteRowsBtn'), matchNamesBtn: document.getElementById('matchNamesBtn'), clearListBtn: document.getElementById('clearListBtn'), - hideAssignedToggle: document.getElementById('hideAssignedToggle'), + addFilteredBtn: document.getElementById('addFilteredBtn'), + renameBtn: document.getElementById('renameBtn'), trackingColsBtn: document.getElementById('trackingColsBtn'), addPartyBtn: document.getElementById('addPartyBtn'), stats: document.getElementById('classifyStats'), }; els.trackingTab.addEventListener('click', function () { showTab('tracking'); }); - if (els.worklistTab) els.worklistTab.addEventListener('click', function () { showTab('worklist'); }); els.transmittalTab.addEventListener('click', function () { showTab('transmittal'); }); if (els.loadWorklistBtn) els.loadWorklistBtn.addEventListener('click', loadWorklist); if (els.pasteRowsBtn) els.pasteRowsBtn.addEventListener('click', function () { openPasteDialog(''); }); if (els.matchNamesBtn) els.matchNamesBtn.addEventListener('click', openMatchDialog); + if (els.addFilteredBtn) els.addFilteredBtn.addEventListener('click', addFilteredFiles); + if (els.renameBtn) els.renameBtn.addEventListener('click', renameInPlace); if (els.clearListBtn) els.clearListBtn.addEventListener('click', function () { var list = C().getWorklist(); - if (!list.length) return; + if (!list.length) { window.zddc.toast('No list rows to clear.', 'info'); return; } // Warn before stranding files that still need a revision: they stay - // assigned (on a "pending" leaf under By tracking number), but the - // row you'd use to finish them here is about to disappear. + // assigned (on a "pending" leaf), but the placeholder row goes away. var pending = 0; list.forEach(function (r) { if (!(r.revisionCell || '').trim()) pending += Object.keys(r.placed || {}).length; }); - if (pending && !confirm(pending + ' file' + (pending === 1 ? '' : 's') + ' still need a revision. They stay assigned (a “pending” folder under By tracking number), but the list row to finish them here goes away. Clear anyway?')) return; + if (pending && !confirm(pending + ' file' + (pending === 1 ? '' : 's') + ' still need a revision. They stay assigned (a “pending” folder), but the list rows to finish them here go away. Clear anyway?')) return; C().clearWorklist(); - window.zddc.toast('List cleared — every assignment is kept (see By tracking number).', 'info'); + window.zddc.toast('List rows cleared — every assignment is kept.', 'info'); }); - if (els.hideAssignedToggle) els.hideAssignedToggle.addEventListener('change', function () { - hideAssigned = !!els.hideAssignedToggle.checked; - if (worklistGrid) worklistGrid.renderBody(); - }); - // Ctrl-V anywhere on the From-a-list panel opens the paste dialog prefilled. - if (els.worklistPanel) els.worklistPanel.addEventListener('paste', function (e) { - if (currentTab !== 'worklist') return; + // Ctrl-V on the By-tracking panel opens the paste dialog prefilled. + if (els.trackingPanel) els.trackingPanel.addEventListener('paste', function (e) { + if (currentTab !== 'tracking') return; if (e.target && e.target.closest('input, textarea')) return; // let real inputs paste var t = (e.clipboardData || window.clipboardData); var text = t ? t.getData('text') : ''; @@ -11480,19 +11420,16 @@ X.B(E,Y);return E}return J}()) } function showTab(which) { - currentTab = (which === 'transmittal' || which === 'worklist') ? which : 'tracking'; + currentTab = (which === 'transmittal') ? 'transmittal' : 'tracking'; els.trackingTab.classList.toggle('active', currentTab === 'tracking'); - if (els.worklistTab) els.worklistTab.classList.toggle('active', currentTab === 'worklist'); els.transmittalTab.classList.toggle('active', currentTab === 'transmittal'); els.trackingPanel.hidden = currentTab !== 'tracking'; - if (els.worklistPanel) els.worklistPanel.hidden = currentTab !== 'worklist'; els.transmittalPanel.hidden = currentTab !== 'transmittal'; render(); // The source-tree Show filters are per-axis, so the visible set changes // with the active tab — re-render the left tree. reRenderSource(); } - // "From a list" drops materialize tracking placements, so its axis is 'tracking'. function activeAxis() { return currentTab === 'transmittal' ? 'transmittal' : 'tracking'; } function reRenderSource() { if (window.app.modules.tree && window.app.modules.tree.render) window.app.modules.tree.render(); } @@ -11504,21 +11441,40 @@ X.B(E,Y);return E}return J}()) var placed = buildPlaced(files); renderTrackingGrid(els.trackingTree); renderTransmittalInto(els.transmittalTree, C().getTransmittalTree(), placed.transmittal); - renderWorklist(placed.byTracking); renderStats(files); } + // Files in the grid whose NAME is complete (tracking + rev + title) — the + // candidates for an in-place rename, regardless of transmittal. + function renameableFiles() { + var c = C(), out = []; + c.trackingGridKeys().forEach(function (k) { + var f = fileByKey(k); if (!f) return; + if (!nameErrors(c.deriveTarget(f)).length) out.push(f); + }); + return out; + } + function renderStats(files) { var s = C().stats(files); if (els.stats) { els.stats.textContent = s.done + ' done · ' + s.partial + ' in progress · ' + s.none + ' unassigned · ' + s.excluded + ' excluded'; } + // Copy… lives on the transmittal tab — enabled once files are fully done + // (tracking leaf AND transmittal). var copyBtn = document.getElementById('copyOutputBtn'); if (copyBtn) { copyBtn.disabled = s.done === 0; copyBtn.textContent = s.done ? ('Copy ' + s.done + '…') : 'Copy…'; } + // Rename… lives on the By-tracking tab — enabled once any grid file has a + // complete name (transmittal not required). + if (els.renameBtn) { + var n = renameableFiles().length; + els.renameBtn.disabled = n === 0; + els.renameBtn.textContent = n ? ('Rename ' + n + '…') : 'Rename…'; + } } function el(tag, cls, text) { @@ -11595,12 +11551,16 @@ X.B(E,Y);return E}return J}()) // seltable's own persistKey storage (merged, not clobbered). ─────────── var GRID_COL_META = [ { id: 'status', title: 'Status', fixed: true }, // fixed = never hidden by the chooser - { id: 'orig', title: 'Original name' }, + { id: 'orig', title: 'Original / expected name' }, { id: 'tn', title: 'Tracking number' }, { id: 'rev', title: 'Rev (status)' }, { id: 'title', title: 'Title' }, + { id: 'src', title: 'Source', defaultHidden: true }, // folded in from "From a list" + { id: 'latest', title: 'Latest rev', defaultHidden: true }, { id: 'x', title: '', fixed: true }, ]; + // A column is hidden if the prefs say so explicitly, else by its defaultHidden. + function colHidden(meta, hidden) { return (meta.id in hidden) ? !!hidden[meta.id] : !!meta.defaultHidden; } var GRID_PREFS_KEY = 'zddc.classifier.trackingCols'; function gridPrefs() { try { return JSON.parse(localStorage.getItem(GRID_PREFS_KEY)) || {}; } catch (_) { return {}; } } function saveGridPrefs(p) { try { localStorage.setItem(GRID_PREFS_KEY, JSON.stringify(p)); } catch (_) { /* private mode */ } } @@ -11624,6 +11584,25 @@ X.B(E,Y);return E}return J}()) // different tab, so its "not placed in a transmittal" error doesn't count here. function nameErrors(d) { return (d.errors || []).filter(function (e) { return e.indexOf('transmittal') === -1; }); } + // ── unified rows: file rows + placeholder rows ───────────────────────── + // A row is a boring 0-or-1-file thing: + // { kind:'file', file, wl:null, id:'f:'+srcKey } + // { kind:'placeholder', file:null, wl, id:'p:'+rowId } (a list row with no + // file yet — drop/match a file on it and it becomes a file row) + function joinName(f) { return f.originalFilename + (f.extension ? '.' + f.extension : ''); } + function isFile(row) { return row.kind === 'file'; } + function gridRows() { + var c = C(), out = []; + c.trackingGridKeys().forEach(function (k) { + var f = fileByKey(k); if (f) out.push({ kind: 'file', file: f, wl: null, id: 'f:' + k }); + }); + c.getWorklist().forEach(function (r) { + if (!Object.keys(r.placed || {}).length) out.push({ kind: 'placeholder', file: null, wl: r, id: 'p:' + r.id }); + }); + return out; + } + function sourceStr(r) { var s = r.source || {}; return [s.mdl ? 'mdl' : '', s.archive ? 'arch' : '', s.pasted ? 'pasted' : ''].filter(Boolean).join(' '); } + // ── per-column cell renderers ────────────────────────────────────────── function gridStatusCell(td, f) { var c = C(), key = c.srcKeyForFile(f), d = c.deriveTarget(f), conflict = c.hasHashConflict(key); @@ -11632,63 +11611,88 @@ X.B(E,Y);return E}return J}()) badge.title = (conflict ? 'Same tracking+revision as another file but DIFFERENT content. ' : '') + (ne.length ? ne.join('; ') : 'Complete'); td.appendChild(badge); } + function gridPlaceholderStatus(td) { + var dot = el('span', 'tfile__badge tg-wanted', '◇'); + dot.title = 'Awaiting a file — drag one onto this row, or use ⚡ Match names.'; + td.appendChild(dot); + } function gridOrigCell(td, f) { - var key = C().srcKeyForFile(f); - var orig = f.originalFilename + (f.extension ? '.' + f.extension : ''); + var key = C().srcKeyForFile(f), orig = joinName(f); var link = el('a', 'tg-orig__link', orig); link.href = '#'; link.title = 'Preview ' + orig; link.addEventListener('click', function (e) { e.preventDefault(); previewKey(key); }); td.appendChild(link); } + function gridExpectedCell(td, wl) { + var name = (wl.currentName || '').trim(); + var span = el('span', 'tg-expected', name || '(drag a file here)'); + span.title = name ? ('Expected file: ' + name + ' — drag it on, or ⚡ Match names.') : 'Drag a file onto this row to name it.'; + td.appendChild(span); + } + // Editable cell for a FILE row → writes the file's identity (placement). function gridEditCell(td, colId, f) { var c = C(), key = c.srcKeyForFile(f), ident = currentIdent(f); var value = colId === 'tn' ? ident.tracking : colId === 'rev' ? ident.rev : ident.title; var ph = colId === 'tn' ? 'ACME-…-0001' : colId === 'rev' ? 'A (IFR)' : 'title'; var warn = colId === 'tn' ? gridTnWarn(ident.tracking) : ''; - var inp = el('input', 'tg-input' + (warn ? ' is-warn' : '')); - inp.type = 'text'; inp.value = value || ''; inp.placeholder = ph; inp.spellcheck = false; - inp.setAttribute('data-no-select', ''); // a click in the input must not toggle row selection - if (warn) inp.title = warn; - inp.addEventListener('change', function () { + editCell(td, 'tg-input', value, ph, function (v) { var cur = currentIdent(f); // re-read so a prior edit isn't clobbered - if (colId === 'tn') cur.tracking = inp.value.trim(); - else if (colId === 'rev') cur.rev = inp.value.trim(); - else cur.title = inp.value; + if (colId === 'tn') cur.tracking = v; + else if (colId === 'rev') cur.rev = v; + else cur.title = v; c.setFileIdentity(key, cur); - }); - td.appendChild(inp); + }, warn); + } + // Editable cell for a PLACEHOLDER row → writes the worklist row. + function gridRowEditCell(td, colId, wl) { + var c = C(); + if (colId === 'tn') editCell(td, 'tg-input', wl.trackingNumber, 'ACME-…-0001', function (v) { c.setRowTracking(wl.id, v); }, tnWarn(wl)); + else if (colId === 'rev') editCell(td, 'tg-input', wl.revisionCell, 'A (IFR)', function (v) { c.setRevisionCell(wl.id, v); }); + else editCell(td, 'tg-input', wl.title, 'title', function (v) { c.setRowTitle(wl.id, v); }); } function gridRemoveCell(td, f) { var c = C(), key = c.srcKeyForFile(f); - var rm = el('button', 'tnode__act tg-x__btn', '✕'); - rm.title = 'Remove from the grid'; + var rm = el('button', 'tnode__act tg-x__btn', '✕'); rm.title = 'Remove from the grid'; rm.addEventListener('click', function () { c.removeFromTrackingGrid(key); }); td.appendChild(rm); } + function gridRowRemoveCell(td, wl) { + var c = C(); + var rm = el('button', 'tnode__act tg-x__btn', '✕'); rm.title = 'Remove this list row (any assignments are kept)'; + rm.addEventListener('click', function () { c.removeWorklistRow(wl.id); }); + td.appendChild(rm); + } // Build the seltable column array, dropping any the chooser has hidden. Each - // column's `get` feeds sort + filter; `render` paints the (editable) cell. + // column's `get` feeds sort + filter; `render` paints the cell. get/render + // receive the unified row wrapper and dispatch on kind. function trackingColumns() { - var c = C(), hidden = (gridPrefs().hidden || {}); + var hidden = (gridPrefs().hidden || {}); var defs = { status: { key: 'status', title: 'Status', cls: 'tg-status', filterable: false, - get: function (f) { var d = c.deriveTarget(f); return c.hasHashConflict(c.srcKeyForFile(f)) ? 'conflict' : (nameErrors(d).length ? 'incomplete' : 'ok'); }, - render: function (f, td) { gridStatusCell(td, f); } }, - orig: { key: 'orig', title: 'Original name', cls: 'tg-orig', - get: function (f) { return f.originalFilename + (f.extension ? '.' + f.extension : ''); }, - render: function (f, td) { gridOrigCell(td, f); } }, + get: function (r) { if (!isFile(r)) return 'awaiting'; var d = C().deriveTarget(r.file); return C().hasHashConflict(C().srcKeyForFile(r.file)) ? 'conflict' : (nameErrors(d).length ? 'incomplete' : 'ok'); }, + render: function (r, td) { isFile(r) ? gridStatusCell(td, r.file) : gridPlaceholderStatus(td); } }, + orig: { key: 'orig', title: 'Original / expected name', cls: 'tg-orig', + get: function (r) { return isFile(r) ? joinName(r.file) : (r.wl.currentName || ''); }, + render: function (r, td) { isFile(r) ? gridOrigCell(td, r.file) : gridExpectedCell(td, r.wl); } }, tn: { key: 'tn', title: 'Tracking number', cls: 'tg-tn', - get: function (f) { return currentIdent(f).tracking; }, - render: function (f, td) { gridEditCell(td, 'tn', f); } }, + get: function (r) { return isFile(r) ? currentIdent(r.file).tracking : (r.wl.trackingNumber || ''); }, + render: function (r, td) { isFile(r) ? gridEditCell(td, 'tn', r.file) : gridRowEditCell(td, 'tn', r.wl); } }, rev: { key: 'rev', title: 'Rev (status)', cls: 'tg-rev', - get: function (f) { return currentIdent(f).rev; }, - render: function (f, td) { gridEditCell(td, 'rev', f); } }, + get: function (r) { return isFile(r) ? currentIdent(r.file).rev : (r.wl.revisionCell || ''); }, + render: function (r, td) { isFile(r) ? gridEditCell(td, 'rev', r.file) : gridRowEditCell(td, 'rev', r.wl); } }, title: { key: 'title', title: 'Title', cls: 'tg-title', - get: function (f) { return currentIdent(f).title; }, - render: function (f, td) { gridEditCell(td, 'title', f); } }, + get: function (r) { return isFile(r) ? currentIdent(r.file).title : (r.wl.title || ''); }, + render: function (r, td) { isFile(r) ? gridEditCell(td, 'title', r.file) : gridRowEditCell(td, 'title', r.wl); } }, + src: { key: 'src', title: 'Source', cls: 'worklist-src', sortable: false, + get: function (r) { return isFile(r) ? '' : sourceStr(r.wl); }, + render: function (r, td) { if (!isFile(r)) renderSource(r.wl, td); } }, + latest: { key: 'latest', title: 'Latest rev', cls: 'tg-latest', + get: function (r) { return isFile(r) ? '' : latestRevOf(r.wl.archiveRevisions); }, + render: function (r, td) { td.textContent = isFile(r) ? '' : latestRevOf(r.wl.archiveRevisions); } }, x: { key: 'x', title: '', cls: 'tg-x', sortable: false, filterable: false, - render: function (f, td) { gridRemoveCell(td, f); } }, + render: function (r, td) { isFile(r) ? gridRemoveCell(td, r.file) : gridRowRemoveCell(td, r.wl); } }, }; - return GRID_COL_META.filter(function (m) { return !hidden[m.id]; }).map(function (m) { return defs[m.id]; }); + return GRID_COL_META.filter(function (m) { return !colHidden(m, hidden); }).map(function (m) { return defs[m.id]; }); } var trackingGrid = null, trackingColSig = ''; @@ -11699,23 +11703,25 @@ X.B(E,Y);return E}return J}()) trackingColSig = colSig(); trackingGrid = window.app.modules.seltable.create({ container: container, - rows: function () { return c.trackingGridKeys().map(fileByKey).filter(Boolean); }, - rowId: function (f) { return c.srcKeyForFile(f); }, + rows: gridRows, + rowId: function (r) { return r.id; }, columns: trackingColumns(), persistKey: GRID_PREFS_KEY, + // Drops are handled at the container level (setupGridDrop) so a + // multi-file drop can fan out over several rows with a live indicator. }); trackingGrid.render(); return trackingGrid; } function renderTrackingGrid(container) { // Empty ↔ populated transition: tear the seltable down for the prompt, - // re-create it (create-once) when files arrive — same as the worklist. - if (!C().trackingGridKeys().length) { + // re-create it (create-once) when rows arrive. + if (!gridRows().length) { trackingGrid = null; container.textContent = ''; container.classList.remove('seltable'); container.appendChild(el('div', 'target-empty', - 'No files yet — drag files here from the left, then type each one’s tracking number, revision, and title. A file that’s already ZDDC-named fills in automatically.')); + 'No files yet — drag files in (or “⊕ Add filtered files”), or “⊞ Load…” / “⎘ Paste rows…” a list of tracking numbers and drop the matching files on. A file that’s already ZDDC-named fills in automatically.')); return; } // The Columns ▾ chooser changes which columns exist → rebuild on mismatch @@ -11726,19 +11732,64 @@ X.B(E,Y);return E}return J}()) // (setFilter re-renders the body, so the rows are always fresh on render). trackingGrid.setFilter(rfTerms.join(' ')); } + // The placeholder rows currently shown in the grid, in DISPLAY (DOM) order — + // the same order the fill indicator highlights, so fill and preview agree. + function visiblePlaceholderIds() { + if (!els.trackingTree) return []; + return Array.prototype.filter.call(els.trackingTree.querySelectorAll('.seltable__row'), function (r) { + return r.dataset.id && r.dataset.id.indexOf('p:') === 0; + }).map(function (r) { return r.dataset.id; }); + } + // Drop N dragged files onto a starting placeholder row → bind file[i] to the + // i-th consecutive PLACEHOLDER row from there (Excel-style column fill). A + // single file just binds to the one row. Worklist rows are looked up by id + // (from the model), so a re-render between binds doesn't disturb the loop. + function fillFromRow(startId, keys) { + var c = C(), ids = visiblePlaceholderIds(), start = ids.indexOf(startId); + if (start < 0) { var row0 = c.getWorklistRow(startId.slice(2)); if (row0) c.assignFromRow(keys, row0); return; } + for (var i = 0; i < keys.length && (start + i) < ids.length; i++) { + var wl = c.getWorklistRow(ids[start + i].slice(2)); + if (wl) c.assignFromRow([keys[i]], wl); + } + } - // Drag files onto the grid → add as rows; auto-fill any already ZDDC-named. + // Drag files onto the grid. Over a PLACEHOLDER row, N dragged files preview + + // fill N consecutive placeholder rows (the fill indicator highlights exactly + // those rows). Over empty space / a file row, they're added as new grid rows. function setupGridDrop(container) { + function placeholderUnder(e) { + var tr = e.target.closest && e.target.closest('.seltable__row'); + return (tr && tr.dataset.id && tr.dataset.id.indexOf('p:') === 0) ? tr : null; + } + function clearFill() { + Array.prototype.forEach.call(container.querySelectorAll('.tg-fill-target'), function (el) { el.classList.remove('tg-fill-target'); }); + container.classList.remove('tg-drop-hover'); + } container.addEventListener('dragover', function (e) { if (!window.app.modules.dnd.active()) return; - e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; container.classList.add('tg-drop-hover'); + e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; + clearFill(); + var tr = placeholderUnder(e); + if (tr) { + var n = (window.app.modules.dnd.getDrag() || []).length || 1; + var rows = Array.prototype.filter.call(container.querySelectorAll('.seltable__row'), function (r) { + return r.dataset.id && r.dataset.id.indexOf('p:') === 0; + }); + var start = rows.indexOf(tr); + for (var i = 0; i < n && (start + i) < rows.length; i++) rows[start + i].classList.add('tg-fill-target'); + } else { + container.classList.add('tg-drop-hover'); + } }); - container.addEventListener('dragleave', function (e) { if (e.target === container) container.classList.remove('tg-drop-hover'); }); + container.addEventListener('dragleave', function (e) { if (e.target === container) clearFill(); }); container.addEventListener('drop', function (e) { - container.classList.remove('tg-drop-hover'); e.preventDefault(); + var tr = placeholderUnder(e); + clearFill(); var keys = window.app.modules.dnd.getDrag(); window.app.modules.dnd.clearDrag(); - if (keys.length) onGridDrop(keys); + if (!keys.length) return; + if (tr) fillFromRow(tr.dataset.id, keys); + else onGridDrop(keys); }); } function onGridDrop(keys) { @@ -11760,10 +11811,10 @@ X.B(E,Y);return E}return J}()) GRID_COL_META.forEach(function (col) { if (col.fixed) return; var lbl = el('label', 'col-chooser__item'); - var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !hidden[col.id]; + var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !colHidden(col, hidden); cb.addEventListener('change', function () { var p = gridPrefs(); p.hidden = p.hidden || {}; - if (cb.checked) delete p.hidden[col.id]; else p.hidden[col.id] = true; + p.hidden[col.id] = !cb.checked; // explicit (overrides defaultHidden) saveGridPrefs(p); trackingGrid = null; // column set changed → rebuild the seltable render(); @@ -11869,54 +11920,53 @@ X.B(E,Y);return E}return J}()) return form; } - // ── "From a list" (scratch worklist via the shared seltable) ──────────── - function renderWorklist(placedByTracking) { - worklistPlaced = placedByTracking || {}; - if (!C().getWorklist().length) { - worklistGrid = null; - els.worklistTable.textContent = ''; - els.worklistTable.appendChild(el('div', 'target-empty', 'Empty — “Load…” numbers from the archive/MDL, “Paste rows…” from Excel, or “⚡ Match names”. Then drag files onto a row to name them. The list is a scratch pad — clearing it keeps every assignment (see By tracking number).')); + function setGridStatus(text) { + var s = document.getElementById('scanStatus'); + if (s) { s.textContent = text; s.classList.toggle('scanning', !!text); } + } + + // "⊕ Add filtered files" — pull every file the LEFT tree filter currently + // shows into the grid (across collapsed folders too); already-ZDDC-named files + // fill in automatically (onGridDrop does the parse). + function addFilteredFiles() { + var tree = window.app.modules.tree; + var files = (tree && tree.filteredFiles) ? tree.filteredFiles() : []; + if (!files.length) { window.zddc.toast('No files to add — the tree filter shows none.', 'info'); return; } + onGridDrop(files.map(function (f) { return C().srcKeyForFile(f); })); + window.zddc.toast('Added ' + files.length + ' file' + (files.length === 1 ? '' : 's') + ' to the grid.', 'success'); + } + + // "Rename…" — rename the grid's NAME-COMPLETE files ON DISK, in place. + // DESTRUCTIVE: no backup. A renamed file is now correctly named, so it leaves + // the grid (forgetFile). Resumable: already-correct files are skipped. + async function renameInPlace() { + var c = C(); + var ready = renameableFiles(); + var items = ready.map(function (f) { + return { file: f, oldKey: c.srcKeyForFile(f), newName: c.deriveTarget(f).filename }; + }).filter(function (x) { return x.newName && x.newName !== joinName(x.file); }); + if (!items.length) { + window.zddc.toast(ready.length ? 'Those files are already correctly named.' : 'No name-complete files to rename — fill in tracking number, revision and title.', 'info'); return; } - ensureWorklistGrid(); - worklistGrid.renderBody(); - } - function rowPlaced(r) { var f = worklistPlaced[r.trackingNumber]; return f && f.length ? f : null; } - function ensureWorklistGrid() { - if (worklistGrid) return worklistGrid; - var c = C(); - var cols = [ - { key: 'tn', title: 'Tracking number', cls: 'worklist-tn', get: function (r) { return r.trackingNumber || ''; }, - render: function (r, td) { editCell(td, 'worklist-tn__input', r.trackingNumber, 'ACME-…-0001', function (v) { c.setRowTracking(r.id, v); }, tnWarn(r)); } }, - { key: 'title', title: 'Title', cls: 'worklist-title', get: function (r) { return r.title || ''; }, - render: function (r, td) { editCell(td, 'worklist-title__input', r.title, 'title', function (v) { c.setRowTitle(r.id, v); }); } }, - { key: 'cur', title: 'Current name', cls: 'worklist-cur', get: function (r) { return r.currentName || ''; } }, - { key: 'src', title: 'Source', cls: 'worklist-src', sortable: false, filterable: false, - get: function (r) { var s = r.source || {}; return [s.mdl ? 'mdl' : '', s.archive ? 'arch' : '', s.pasted ? 'pasted' : ''].filter(Boolean).join(' '); }, - render: function (r, td) { renderSource(r, td); } }, - { key: 'latest', title: 'Latest rev', get: function (r) { return latestRevOf(r.archiveRevisions); } }, - { key: 'rev', title: 'Revision', cls: 'worklist-rev', get: function (r) { return r.revisionCell; }, - render: function (r, td) { editCell(td, 'worklist-rev__input', r.revisionCell, 'A (IFR)', function (v) { c.setRevisionCell(r.id, v); }); } }, - ]; - worklistGrid = window.app.modules.seltable.create({ - container: els.worklistTable, - extraTitle: 'Files', - rows: function () { - var list = c.getWorklist(); - return hideAssigned ? list.filter(function (r) { return !rowPlaced(r); }) : list; - }, - rowId: function (r) { return r.id; }, - columns: cols, - onRowDrop: function (rowId, keys) { var row = c.getWorklistRow(rowId); if (row) c.assignFromRow(keys, row); }, - onActivate: function (ids) { - if (!ids.length) return; - var v = prompt('Set the revision on ' + ids.length + ' selected row(s) (e.g. "A (IFR)"):', ''); - if (v != null) c.setRevisionCells(ids, v.trim()); - }, - rowExtra: function (r, td) { renderWorklistFiles(r, td); }, - }); - worklistGrid.render(); - return worklistGrid; + var preview = items.slice(0, 4).map(function (x) { return ' ' + joinName(x.file) + ' → ' + x.newName; }).join('\n'); + var msg = '⚠ RENAME ' + items.length + ' FILE' + (items.length === 1 ? '' : 'S') + ' IN PLACE — this EDITS YOUR SOURCE FILES on disk.\n\n' + + 'There is NO backup and it cannot be undone. Renamed files are now correctly named, so they leave the grid.\n\n' + + preview + (items.length > 4 ? ('\n …and ' + (items.length - 4) + ' more') : '') + + '\n\nRename these files in place now?'; + if (!confirm(msg)) return; + setGridStatus('Renaming…'); + var done = 0, errors = 0; + for (var i = 0; i < items.length; i++) { + setGridStatus('Renaming… ' + (i + 1) + '/' + items.length + ' — ' + items[i].newName); + try { await window.app.modules.rename.renameTo(items[i].file, items[i].newName); c.forgetFile(items[i].oldKey); done++; } + catch (e) { errors++; window.zddc.toast('Rename failed for ' + items[i].newName + ' — ' + (e.message || e), 'error'); } + } + setGridStatus(''); + render(); + reRenderSource(); + window.zddc.toast('Renamed ' + done + ' file' + (done === 1 ? '' : 's') + ' in place' + + (errors ? (', ' + errors + ' failed (retry)') : '') + '. Source files updated.', errors ? 'warning' : 'success'); } // An editable seltable cell: an that commits on change. `warn` is an // optional tooltip that flags (without blocking) a questionable value. @@ -11947,30 +11997,6 @@ X.B(E,Y);return E}return J}()) td.appendChild(el('span', 'src-badge src-badge--pasted', 'pasted')); } } - function renderWorklistFiles(row, td) { - var c = C(), files = rowPlaced(row) || []; - files.forEach(function (f) { - var d = c.deriveTarget(f); - var a = c.getAssignment(d.key) || {}; - var line = el('div', 'tfile' + (d.errors.length ? ' tfile--err' : '')); - line.dataset.key = d.key; line.draggable = true; - line.addEventListener('dragstart', function (e) { window.app.modules.dnd.setDrag([d.key], e); }); - var nm = el('span', 'mdlfile__name', d.filename || '(set a revision)'); - nm.title = 'from ' + f.originalFilename + (f.extension ? '.' + f.extension : ''); - line.appendChild(nm); - var usingRow = a.titleOverride != null && row.title && a.titleOverride === row.title.trim(); - var tgl = el('button', 'tnode__act', usingRow ? 'Title: row' : 'Title: file'); - tgl.title = 'Use the row’s title or the file’s own'; - tgl.addEventListener('click', function () { c.setTitleOverride(d.key, usingRow ? '' : row.title); }); - line.appendChild(tgl); - var rm = el('button', 'tnode__act tfile__remove', '✕'); - rm.title = 'Remove this file from the row'; - rm.addEventListener('click', function () { c.unassignRowFile(row, d.key); }); - line.appendChild(rm); - td.appendChild(line); - }); - } - // "From a list" loader: "Load…" opens a multi-select directory tree (scoped // to the served context); every ticked directory is walked recursively into // the union of existing files + MDL deliverables, deduped by tracking number @@ -12070,7 +12096,7 @@ X.B(E,Y);return E}return J}()) function finishLoad(rows) { listScanned = true; C().appendWorklist(rows); // APPEND — the list accumulates across batches - showTab('worklist'); + showTab('tracking'); window.zddc.toast(rows.length ? ('Added ' + rows.length + ' tracking number' + (rows.length === 1 ? '' : 's') + ' from the selected directories. Drag files on, set revisions.') : 'No files or deliverables in the selected directories.', rows.length ? 'success' : 'warning'); @@ -12151,7 +12177,7 @@ X.B(E,Y);return E}return J}()) add.addEventListener('click', function () { var n = parsed.rows.length; c.appendWorklist(parsed.rows); - m.close(); showTab('worklist'); + m.close(); showTab('tracking'); var assigned = autoAssignByName(); var msg = 'Added ' + n + ' pasted row' + (n === 1 ? '' : 's') + '.'; if (assigned) msg += ' Auto-assigned ' + assigned + ' file' + (assigned === 1 ? '' : 's') + ' by current name.'; @@ -12203,7 +12229,7 @@ X.B(E,Y);return E}return J}()) var p = proposals[Number(cb.dataset.i)]; if (p) { c.assignFromRow([c.srcKeyForFile(p.file)], p.row); n++; } }); - m.close(); showTab('worklist'); + m.close(); showTab('tracking'); window.zddc.toast('Assigned ' + n + ' file' + (n === 1 ? '' : 's') + ' by name match.', n ? 'success' : 'info'); }); fuzzy.addEventListener('change', function () { opts.fuzzy = fuzzy.checked; refresh(); }); @@ -12828,6 +12854,90 @@ X.B(E,Y);return E}return J}()) }; })(); +/** + * ZDDC Classifier — in-place rename engine. + * + * Renames SOURCE files to their canonical ZDDC names, ON DISK, IN PLACE. + * DESTRUCTIVE — there is no backup. HTTP-backed handles (zddc-server) take the + * atomic server-side move (single round-trip); local File System Access handles + * copy+remove (the API has no native rename verb). The source folder is the only + * thing written. + * + * Resumable + boring: a file already at its target name is skipped, so a re-run + * after an interruption only renames what's left. One file in, one file out. + * + * Lifted out of the old Rename-in-place spreadsheet so the By-Tracking grid can + * drive the same, already-proven rename path. + */ +(function () { + 'use strict'; + + // The folder handle holding `file`. Fresh-scan files carry it; snapshot-loaded + // files resolve it (and their own handle) lazily from the workspace root. + async function folderHandleFor(file) { + if (file.folderHandle) return file.folderHandle; + if (window.app.modules.scanner && window.app.modules.scanner.resolveFileHandle && window.app.rootHandle) { + await window.app.modules.scanner.resolveFileHandle(window.app.rootHandle, file); + if (file.folderHandle) return file.folderHandle; + } + throw new Error('source folder not connected'); + } + + // Rename one file in place to `newName`. Returns 'renamed' | 'skipped'. + // Mutates the in-memory file object to its NEW identity (originalFilename / + // extension / handle) so the rest of the app sees the renamed file. + async function renameTo(file, newName) { + var oldName = window.zddc.joinExtension(file.originalFilename, file.extension); + if (oldName === newName) return 'skipped'; + if (file.isVirtual) throw new Error('cannot rename a file inside a ZIP — extract it first'); + + var folder = await folderHandleFor(file); + var perm = await folder.queryPermission({ mode: 'readwrite' }); + if (perm !== 'granted') { + var granted = await folder.requestPermission({ mode: 'readwrite' }); + if (granted !== 'granted') throw new Error('write permission denied'); + } + + var src = window.zddc.source; + if (src && src.isHttpHandle && src.isHttpHandle(folder)) { + var base = new URL(folder.url()).pathname; + await src.moveFile(base + encodeURIComponent(oldName), base + encodeURIComponent(newName)); + file.handle = await folder.getFileHandle(newName); + } else { + var oldHandle = await folder.getFileHandle(oldName); + var data = await oldHandle.getFile(); + var newHandle = await folder.getFileHandle(newName, { create: true }); + var w = await newHandle.createWritable(); + await w.write(data); + await w.close(); + await folder.removeEntry(oldName); + file.handle = newHandle; + } + + var split = window.zddc.splitExtension(newName); + file.originalFilename = split.name; + file.extension = split.extension; + return 'renamed'; + } + + // Rename a batch of { file, newName } items. Returns { renamed, skipped, errors }. + // onProgress(done, total, name) is called before each file. + async function runInPlace(items, onProgress) { + var s = { renamed: 0, skipped: 0, errors: 0 }; + for (var i = 0; i < items.length; i++) { + if (onProgress) onProgress(i + 1, items.length, items[i].newName); + try { s[await renameTo(items[i].file, items[i].newName)]++; } + catch (e) { + s.errors++; + if (window.zddc && window.zddc.toast) window.zddc.toast('Rename failed for ' + items[i].newName + ' — ' + (e.message || e), 'error'); + } + } + return s; + } + + window.app.modules.rename = { renameTo: renameTo, runInPlace: runInPlace }; +})(); + /** * Spreadsheet Module * Handles table rendering, cell editing, and file operations diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index cddafb9..522326c 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -1793,7 +1793,7 @@ body {
ZDDC - v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a + v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
diff --git a/zddc/internal/apps/embedded/transmittal.html b/zddc/internal/apps/embedded/transmittal.html index 21adf15..1432fbd 100644 --- a/zddc/internal/apps/embedded/transmittal.html +++ b/zddc/internal/apps/embedded/transmittal.html @@ -2770,7 +2770,7 @@ dialog.modal--narrow {
ZDDC Transmittal - v0.0.27-beta · 2026-06-16 13:52:23 · 1bb5d1a + v0.0.27-beta · 2026-06-16 19:19:05 · 054cf2d
JavaScript not available