From dbb7f6c9b58d5e120ec6c9c6ddedb7453db359a3 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Mon, 11 May 2026 11:45:14 -0500 Subject: [PATCH] fix(browse): dblclick rescopes client-side instead of hard nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hard navigation to // 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) --- browse/js/app.js | 24 ++++++++++++++++++++++ browse/js/events.js | 50 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/browse/js/app.js b/browse/js/app.js index cdd4198..b6abc6d 100644 --- a/browse/js/app.js +++ b/browse/js/app.js @@ -49,6 +49,9 @@ return augmented; } + // Expose for events.js's client-side rescope on dblclick. + window.app.modules.augmentRoot = withVirtualCanonicals; + async function bootstrap() { events.init(); @@ -67,6 +70,27 @@ + ' 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') { diff --git a/browse/js/events.js b/browse/js/events.js index ec5816a..89f0aa3 100644 --- a/browse/js/events.js +++ b/browse/js/events.js @@ -311,9 +311,16 @@ async function navigateIntoFolder(node) { if (state.source === 'server') { + // Rescope client-side rather than hard-navigating. A hard + // nav would let zddc-server's auto-serve kick in and swap + // us out of browse for canonical folders (e.g. /archive/ + // → archive tool, /staging/ → transmittal). Staying in + // browse is what the user asked for; pushState keeps the + // URL bar accurate so a reload would re-load browse at the + // new scope. var url = window.app.modules.tree.pathFor(node); if (!url.endsWith('/')) url += '/'; - window.location.assign(url); + await rescopeServer(url, node.name); return; } if (state.source === 'fs') { @@ -333,6 +340,47 @@ } } + // Client-side rescope for server mode. Updates the URL via + // history.pushState, fetches the new directory listing, and + // re-renders the tree from scratch. Page DOES NOT reload. + async function rescopeServer(url, displayName) { + var entries; + try { + entries = await loader.fetchServerChildren(url); + } catch (e) { + statusError('Failed to enter ' + displayName + ': ' + (e.message || e)); + return; + } + state.currentPath = url; + // Selection / preview belong to the old scope; clear them so + // the new root doesn't carry stale highlight state. + state.selectedId = null; + state.lastPreviewedNodeId = null; + // Inject virtual canonical folders at the new scope if it's a + // project root. (app.js owns this helper; expose via window.app.) + var augment = window.app.modules.augmentRoot; + var rootEntries = (typeof augment === 'function') + ? augment(entries, url) + : entries; + tree.setRoot(rootEntries); + tree.render(); + // Reset the preview pane so the user sees an "empty selection" + // state at the new scope instead of the previous file. + var previewBody = document.getElementById('previewBody'); + if (previewBody) previewBody.innerHTML = ''; + var previewTitle = document.getElementById('previewTitle'); + if (previewTitle) previewTitle.textContent = 'No file selected'; + var previewMeta = document.getElementById('previewMeta'); + if (previewMeta) previewMeta.textContent = ''; + // pushState so the URL bar reflects the new scope. A real + // reload would re-load browse at this URL (trailing slash → + // ServeDirectory → embedded browse SPA). + try { + history.pushState({ zddcBrowse: true, path: url }, '', url); + } catch (_e) { /* private browsing edge cases */ } + statusInfo('Entered ' + displayName); + } + // Public API window.app.modules.events = { init: init,