ZDDC/browse/js/app.js
ZDDC dbb7f6c9b5 fix(browse): dblclick rescopes client-side instead of hard nav
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>
2026-05-11 11:45:14 -05:00

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