Hard navigation to /<path>/ lets zddc-server's auto-serve kick in for
canonical folders — /archive/ swaps in the archive tool, /staging/ the
transmittal tool, etc. — so double-clicking a canonical folder from
inside browse silently swapped the user out of browse, contrary to
what they expected.
Fix: client-side rescope. navigateIntoFolder() now fetches the new
directory listing via the loader, calls tree.setRoot() with virtual
canonicals re-applied, and pushes the new URL via history.pushState.
The page never reloads. A subsequent reload still works (browse loads
itself at the new URL since trailing-slash → ServeDirectory → embedded
browse SPA).
Side effects:
- augmentRoot (the canonical-folder injection helper) exposed via
window.app.modules so events.js can re-apply it on rescope.
- popstate handler: back/forward in the browser triggers the same
rescope path with the historic URL.
- Selection + preview reset on rescope; the previous file's preview
isn't carried into the new scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
4.3 KiB
JavaScript
101 lines
4.3 KiB
JavaScript
// app.js — bootstrap. Runs after every other module's IIFE has
|
|
// registered its functions on window.app.modules.
|
|
(function () {
|
|
'use strict';
|
|
|
|
var state = window.app.state;
|
|
var loader = window.app.modules.loader;
|
|
var tree = window.app.modules.tree;
|
|
var events = window.app.modules.events;
|
|
|
|
// Canonical folders that should appear at the root of a project
|
|
// view even if they don't yet exist on disk. Matches the four
|
|
// stage cards on the project landing page. zddc-server returns an
|
|
// empty listing for these paths (see commit 3fc3717), so
|
|
// navigating into a virtual folder works without 404.
|
|
var CANONICAL_PROJECT_FOLDERS = ['archive', 'working', 'staging', 'reviewing'];
|
|
|
|
// Decide whether `path` looks like a project root — i.e. exactly
|
|
// one path segment after the leading slash. /Project-1/ → yes;
|
|
// / → no; /Project-1/working/ → no.
|
|
function isProjectRoot(path) {
|
|
if (!path || path === '/') return false;
|
|
var trimmed = path.replace(/^\/+|\/+$/g, '');
|
|
if (!trimmed) return false;
|
|
return trimmed.indexOf('/') < 0;
|
|
}
|
|
|
|
// Merge virtual entries for any canonical folders absent from the
|
|
// server's listing. Each virtual entry is shaped like a normal
|
|
// directory entry so the tree renderer treats it the same way.
|
|
function withVirtualCanonicals(entries, path) {
|
|
if (!isProjectRoot(path)) return entries;
|
|
var present = Object.create(null);
|
|
entries.forEach(function (e) { if (e.isDir) present[e.name] = true; });
|
|
var augmented = entries.slice();
|
|
CANONICAL_PROJECT_FOLDERS.forEach(function (name) {
|
|
if (!present[name]) {
|
|
augmented.push({
|
|
name: name,
|
|
isDir: true,
|
|
size: 0,
|
|
modTime: null,
|
|
ext: '',
|
|
url: path.replace(/\/$/, '') + '/' + name + '/',
|
|
virtual: true
|
|
});
|
|
}
|
|
});
|
|
return augmented;
|
|
}
|
|
|
|
// Expose for events.js's client-side rescope on dblclick.
|
|
window.app.modules.augmentRoot = withVirtualCanonicals;
|
|
|
|
async function bootstrap() {
|
|
events.init();
|
|
|
|
// Try server auto-detect. If this page is served by zddc-server
|
|
// (or any server with a Caddy-shaped JSON listing), load the
|
|
// current directory automatically. Otherwise show the empty
|
|
// state with the "Select Directory" button.
|
|
var detected = await loader.autoDetectServerMode();
|
|
if (detected) {
|
|
var entries = withVirtualCanonicals(detected.entries, detected.path);
|
|
tree.setRoot(entries);
|
|
events.showBrowseRoot();
|
|
tree.render();
|
|
events.statusInfo('Loaded ' + detected.entries.length + ' item'
|
|
+ (detected.entries.length === 1 ? '' : 's')
|
|
+ ' from ' + detected.path);
|
|
}
|
|
// Else: empty state stays visible; user can click Select Directory.
|
|
|
|
// Browser back / forward: client-side rescope when the URL
|
|
// changes via popstate. We can't tell server-vs-fs mode from
|
|
// popstate alone, so only honor it in server mode.
|
|
window.addEventListener('popstate', async function () {
|
|
if (window.app.state.source !== 'server') return;
|
|
var path = location.pathname;
|
|
if (!path.endsWith('/')) path += '/';
|
|
try {
|
|
var es = await loader.fetchServerChildren(path);
|
|
window.app.state.currentPath = path;
|
|
window.app.state.selectedId = null;
|
|
window.app.state.lastPreviewedNodeId = null;
|
|
tree.setRoot(withVirtualCanonicals(es, path));
|
|
tree.render();
|
|
var previewBody = document.getElementById('previewBody');
|
|
if (previewBody) previewBody.innerHTML = '';
|
|
var previewTitle = document.getElementById('previewTitle');
|
|
if (previewTitle) previewTitle.textContent = 'No file selected';
|
|
} catch (_e) { /* swallow — leave the tree as-is */ }
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', bootstrap);
|
|
} else {
|
|
bootstrap();
|
|
}
|
|
})();
|