ZDDC/classifier/js/rename.js
ZDDC f82d6919b4 feat(classifier): one classify surface — By-tracking grid with Rename, transmittal with Copy
Collapse the two-mode tool (Classify & copy / Rename in place) into a single
surface. The top mode toggle and the standalone Rename spreadsheet are gone; the
By-tracking grid is now the one editable table, and the two operations are framed
as the two physical things you can do with a classified file:

- By tracking number → "Rename…": a tracking number + rev + title with no
  transmittal IS a rename, so this renames the name-complete grid files IN PLACE
  on disk (rename.js, lifted from the spreadsheet — HTTP move / FS copy+remove,
  resumable). Blocking red no-backup warning; a renamed file is now correctly
  named so it leaves the grid (classify.forgetFile).
- By transmittal → "Copy…": the existing resumable/verified archive copy, moved
  onto this tab; enabled once files are fully classified (tracking + transmittal).

"From a list" is folded into the By-tracking grid, not a separate tab. The grid
now holds two boring row kinds (one row ↔ 0-or-1 file):
- file rows (workset / placed), edited via setFileIdentity;
- placeholder rows = list rows with no file yet (Load…/Paste rows…/Match names),
  edited via the worklist setters; drop or match a file and it becomes a file row.
Dropping N files on a placeholder fans out over consecutive placeholders
(fillFromRow) — the start of the Excel-style block fill. New "⊕ Add filtered
files" pulls every file the left-tree filter shows into the grid. Source / Latest
rev fold in as optional (default-hidden) columns.

Chrome: removed the mode toggle + spreadsheet pane from the template; severed the
spreadsheet/sort/filter/resize/selection inits in app.js (modules stay bundled,
store still drives folder selection + reset); setMode() is now a no-arg enabler;
welcome tutorial rewritten to the single flow.

Tests: bootstrap via app.setMode() (no mode button); mode-switch test asserts the
single surface; worklist test drives placeholder rows in #trackingTree. 69
classify + 56 classifier/tables/cap green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:53:04 -05:00

83 lines
3.8 KiB
JavaScript

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