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>
83 lines
3.8 KiB
JavaScript
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 };
|
|
})();
|