Foundation for the non-destructive map+copy workflow: source stays read-only,
files are mapped onto two orthogonal target trees, a later step copies renamed
copies to a separate output dir.
- classify.js: the single source of truth. assignments map keyed by
source-relative path (survives re-pick); tracking tree (positional: ancestors
joined '-' = tracking number, immediate parent 'REV (STATUS)' leaf = rev+status,
title from original name) and transmittal tree (<party>/{received,issued}/<bin>).
deriveTarget() computes filename + output path + validation purely; pub/sub +
debounced autosave; node CRUD with dangling-placement cleanup.
- persist.js: IndexedDB store of the serialized map + the source
FileSystemDirectoryHandle, with queryPermission/requestPermission re-grant on
reload and a re-pick fallback.
- tests/classify.spec.js: 9 in-page unit tests for the derive/assignment logic
(no FS Access needed) — tracking join, leaf REV (STATUS) parse incl. invalid
status, title derivation/override, transmittal path composition, exclude,
cascade delete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
4.4 KiB
JavaScript
98 lines
4.4 KiB
JavaScript
/**
|
|
* ZDDC Classifier — persistence for the Classify & Copy map.
|
|
*
|
|
* The assignment map and target trees, plus the picked source directory
|
|
* HANDLE, are stored in IndexedDB (localStorage can't hold a
|
|
* FileSystemDirectoryHandle; the handle is structured-cloneable, so IndexedDB
|
|
* can). On reload we re-request permission on the stored handle — a single
|
|
* click re-grants access, no re-navigation. If that fails (permission denied,
|
|
* or a different machine), the caller falls back to a fresh pick and the map
|
|
* re-attaches by relative path.
|
|
*
|
|
* NOTE: a stored handle is only valid in the same browser profile on the same
|
|
* machine. The map keys on source-relative paths, so re-picking the same tree
|
|
* elsewhere still re-attaches — that's the warning shown to the user on save.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var DB_NAME = 'zddc-classifier';
|
|
var STORE = 'kv';
|
|
var K_STATE = 'classify-state';
|
|
var K_HANDLE = 'source-handle';
|
|
|
|
var available = typeof indexedDB !== 'undefined';
|
|
|
|
function openDB() {
|
|
return new Promise(function (resolve, reject) {
|
|
if (!available) { reject(new Error('IndexedDB unavailable')); return; }
|
|
var req = indexedDB.open(DB_NAME, 1);
|
|
req.onupgradeneeded = function () {
|
|
var db = req.result;
|
|
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
|
|
};
|
|
req.onsuccess = function () { resolve(req.result); };
|
|
req.onerror = function () { reject(req.error); };
|
|
});
|
|
}
|
|
|
|
function tx(mode, fn) {
|
|
return openDB().then(function (db) {
|
|
return new Promise(function (resolve, reject) {
|
|
var t = db.transaction(STORE, mode);
|
|
var store = t.objectStore(STORE);
|
|
var out = fn(store);
|
|
t.oncomplete = function () { resolve(out && out.result !== undefined ? out.result : out); };
|
|
t.onerror = function () { reject(t.error); };
|
|
t.onabort = function () { reject(t.error); };
|
|
});
|
|
});
|
|
}
|
|
|
|
function put(key, value) { return tx('readwrite', function (s) { return s.put(value, key); }); }
|
|
|
|
function getValue(key) {
|
|
return openDB().then(function (db) {
|
|
return new Promise(function (resolve, reject) {
|
|
var t = db.transaction(STORE, 'readonly');
|
|
var req = t.objectStore(STORE).get(key);
|
|
req.onsuccess = function () { resolve(req.result); };
|
|
req.onerror = function () { reject(req.error); };
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── public API ─────────────────────────────────────────────────────────
|
|
function saveState(obj) { return put(K_STATE, obj).catch(function (e) { console.warn('persist.saveState', e); }); }
|
|
function loadState() { return getValue(K_STATE).catch(function () { return null; }); }
|
|
|
|
function saveSourceHandle(handle) {
|
|
// Real FileSystemDirectoryHandle only; the HTTP polyfill handle is not
|
|
// worth persisting (server mode re-detects the root on load).
|
|
if (!handle || handle.isHttp) return Promise.resolve();
|
|
return put(K_HANDLE, handle).catch(function (e) { console.warn('persist.saveHandle', e); });
|
|
}
|
|
function loadSourceHandle() { return getValue(K_HANDLE).catch(function () { return null; }); }
|
|
|
|
function clearAll() {
|
|
return tx('readwrite', function (s) { s.delete(K_STATE); s.delete(K_HANDLE); })
|
|
.catch(function (e) { console.warn('persist.clear', e); });
|
|
}
|
|
|
|
// Re-acquire read permission on a stored handle. Returns true if usable.
|
|
function verifyPermission(handle) {
|
|
if (!handle || typeof handle.queryPermission !== 'function') return Promise.resolve(false);
|
|
var opts = { mode: 'read' };
|
|
return handle.queryPermission(opts).then(function (p) {
|
|
if (p === 'granted') return true;
|
|
return handle.requestPermission(opts).then(function (p2) { return p2 === 'granted'; });
|
|
}).catch(function () { return false; });
|
|
}
|
|
|
|
window.app.modules.persist = {
|
|
available: available,
|
|
saveState: saveState, loadState: loadState,
|
|
saveSourceHandle: saveSourceHandle, loadSourceHandle: loadSourceHandle,
|
|
verifyPermission: verifyPermission, clearAll: clearAll,
|
|
};
|
|
})();
|