feat(browse): render default_tool=tables dirs (mdl/rsk/ssr) as click-to-table leaves
A directory whose cascade default_tool is "tables" (mdl/rsk/ssr and any operator-configured table dir) now shows in the browse tree as a leaf with a table icon — no expand chevron — and clicking it opens the tables tool in the preview pane (an iframe scoped to the dir, mirroring grid.js's classifier embed) instead of expanding/navigating into the folder. Detection rides the cascade, not hardcoded names: the directory listing now carries a per-entry default_tool hint (listing.FileInfo.DefaultTool, set via zddc.DefaultToolAt for both on-disk children and the virtual canonical peers mdl/rsk/ssr). Browse's util.isTableLeaf(node) keys off it; tree.js renders the leaf, events.js routes its click/Enter to the preview (excluding it from expand/navigate), and preview.js renders the iframe at the dir's NO-SLASH URL (the default_tool route — <dir>/tables.html 404s for a virtual dir). Server mode only (the hint is absent on file://, so offline folders stay ordinary expandable dirs). Validated end-to-end in a containerized browser: mdl/rsk/ssr are leaves, normal folders keep their chevrons, and clicking mdl loads the tables view inline without navigating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
70591dcfa6
commit
9b20e4451f
7 changed files with 90 additions and 5 deletions
|
|
@ -353,7 +353,10 @@
|
||||||
var node = state.nodes.get(id);
|
var node = state.nodes.get(id);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
var isExpandable = row.dataset.isdir === 'true' || row.dataset.iszip === 'true';
|
// Table-leaf dirs (mdl/rsk/ssr) are NOT expandable — they fall
|
||||||
|
// through to the preview path, which opens the tables tool.
|
||||||
|
var isExpandable = (row.dataset.isdir === 'true' || row.dataset.iszip === 'true')
|
||||||
|
&& row.dataset.tableleaf !== 'true';
|
||||||
|
|
||||||
if (isExpandable) {
|
if (isExpandable) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -419,6 +422,7 @@
|
||||||
var row = e.target.closest('.tree-row');
|
var row = e.target.closest('.tree-row');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
if (row.dataset.isdir !== 'true') return;
|
if (row.dataset.isdir !== 'true') return;
|
||||||
|
if (row.dataset.tableleaf === 'true') return; // leaf: single-click previews
|
||||||
var id = parseInt(row.dataset.id, 10);
|
var id = parseInt(row.dataset.id, 10);
|
||||||
var node = state.nodes.get(id);
|
var node = state.nodes.get(id);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
@ -490,7 +494,10 @@
|
||||||
var curIdx = visible.indexOf(state.selectedId);
|
var curIdx = visible.indexOf(state.selectedId);
|
||||||
var node = state.selectedId != null
|
var node = state.selectedId != null
|
||||||
? state.nodes.get(state.selectedId) : null;
|
? state.nodes.get(state.selectedId) : null;
|
||||||
var expandable = !!(node && (node.isDir || node.isZip));
|
// Table-leaf dirs aren't expandable: Enter/Space previews them
|
||||||
|
// (opens the table) rather than toggling.
|
||||||
|
var expandable = !!(node && (node.isDir || node.isZip)
|
||||||
|
&& !window.app.modules.util.isTableLeaf(node));
|
||||||
var nextId = null;
|
var nextId = null;
|
||||||
var previewModule = previewMod();
|
var previewModule = previewMod();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,11 @@
|
||||||
// context-menu affordance (server mode only — offline has no
|
// context-menu affordance (server mode only — offline has no
|
||||||
// authenticated identity to attribute saves to).
|
// authenticated identity to attribute saves to).
|
||||||
history: !!e.history,
|
history: !!e.history,
|
||||||
|
// Server-computed: cascade-resolved default tool for a DIRECTORY
|
||||||
|
// entry (e.g. "tables", "classifier"). Browse renders a dir whose
|
||||||
|
// defaultTool=="tables" (mdl/rsk/ssr) as a click-to-table leaf —
|
||||||
|
// the table opens in the preview pane instead of the dir expanding.
|
||||||
|
defaultTool: (typeof e.default_tool === 'string') ? e.default_tool : '',
|
||||||
// FS-API specific (null in server mode):
|
// FS-API specific (null in server mode):
|
||||||
handle: null
|
handle: null
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -469,12 +469,47 @@
|
||||||
// ── Public entry ────────────────────────────────────────────────────────
|
// ── Public entry ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function showFilePreview(node, opts) {
|
async function showFilePreview(node, opts) {
|
||||||
if (node.isDir) return;
|
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
// Table-leaf dirs (mdl/rsk/ssr, default_tool=tables) open the tables
|
||||||
|
// tool inline in the preview pane instead of expanding/navigating.
|
||||||
|
if (window.app.modules.util.isTableLeaf(node)) return renderTableLeaf(node);
|
||||||
|
if (node.isDir) return;
|
||||||
if (opts.popup) return renderInPopup(node);
|
if (opts.popup) return renderInPopup(node);
|
||||||
return renderInline(node, opts);
|
return renderInline(node, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderTableLeaf embeds the tables tool for a default_tool=tables
|
||||||
|
// directory as an iframe scoped to that dir — the same in-pane tool
|
||||||
|
// embed pattern grid.js uses for classifier. Server mode only (the
|
||||||
|
// default_tool listing hint that flags a table-leaf is absent offline,
|
||||||
|
// so this never fires on file:// — the dir stays an ordinary folder).
|
||||||
|
function renderTableLeaf(node) {
|
||||||
|
disposeEditors();
|
||||||
|
var container = document.getElementById('previewBody');
|
||||||
|
var titleEl = document.getElementById('previewTitle');
|
||||||
|
var metaEl = document.getElementById('previewMeta');
|
||||||
|
var popoutBtn = document.getElementById('previewPopout');
|
||||||
|
if (!container) return;
|
||||||
|
if (titleEl) titleEl.textContent = node.displayName || node.name;
|
||||||
|
if (metaEl) metaEl.textContent = 'table';
|
||||||
|
if (popoutBtn) popoutBtn.classList.add('hidden');
|
||||||
|
if (window.app.state.source !== 'server' || !node.url) {
|
||||||
|
renderEmpty(container, 'Table view is available in server mode.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The tables tool is served at the dir's NO-SLASH URL (the cascade's
|
||||||
|
// default_tool routing). The trailing-slash form would serve the
|
||||||
|
// browse listing instead, and <dir>/tables.html 404s for a virtual
|
||||||
|
// dir (mdl/rsk/ssr have no on-disk folder). So strip the slash.
|
||||||
|
var src = node.url.replace(/\/+$/, '');
|
||||||
|
container.innerHTML = '';
|
||||||
|
var frame = document.createElement('iframe');
|
||||||
|
frame.className = 'preview-iframe';
|
||||||
|
frame.src = src;
|
||||||
|
frame.setAttribute('title', 'Table: ' + (node.displayName || node.name));
|
||||||
|
container.appendChild(frame);
|
||||||
|
}
|
||||||
|
|
||||||
window.app.modules.preview = {
|
window.app.modules.preview = {
|
||||||
showFilePreview: showFilePreview,
|
showFilePreview: showFilePreview,
|
||||||
// Tear down any live editor + blank the pane (rescope / popstate).
|
// Tear down any live editor + blank the pane (rescope / popstate).
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,12 @@
|
||||||
// undefined — Caddy / FS-API listings (no verbs field).
|
// undefined — Caddy / FS-API listings (no verbs field).
|
||||||
// Per-entry gates skip the cascade check
|
// Per-entry gates skip the cascade check
|
||||||
// and fall back to canMutate / writable.
|
// and fall back to canMutate / writable.
|
||||||
verbs: typeof raw.verbs === 'string' ? raw.verbs : undefined
|
verbs: typeof raw.verbs === 'string' ? raw.verbs : undefined,
|
||||||
|
// Cascade default tool for a directory entry. When "tables"
|
||||||
|
// (mdl/rsk/ssr), the node is a TABLE LEAF: rendered without a
|
||||||
|
// chevron and, on click, opens the tables tool in the preview
|
||||||
|
// pane instead of expanding/navigating. See isTableLeaf().
|
||||||
|
defaultTool: raw.defaultTool || ''
|
||||||
};
|
};
|
||||||
state.nodes.set(id, node);
|
state.nodes.set(id, node);
|
||||||
return node;
|
return node;
|
||||||
|
|
@ -275,6 +280,8 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
function symbolForNode(node) {
|
function symbolForNode(node) {
|
||||||
|
// Table-leaf dirs (mdl/rsk/ssr) read as a table, not a folder.
|
||||||
|
if (window.app.modules.util.isTableLeaf(node)) return 'icon-file-spreadsheet';
|
||||||
if (node.isDir) return 'icon-folder';
|
if (node.isDir) return 'icon-folder';
|
||||||
if (node.isZip) return 'icon-folder-archive';
|
if (node.isZip) return 'icon-folder-archive';
|
||||||
// `.zddc` (no extension) is the cascade config — same family
|
// `.zddc` (no extension) is the cascade config — same family
|
||||||
|
|
@ -351,7 +358,10 @@
|
||||||
// via the events.js click handler (it sees the modifier key).
|
// via the events.js click handler (it sees the modifier key).
|
||||||
function rowHtml(node) {
|
function rowHtml(node) {
|
||||||
var indent = 0.4 + node.depth * 1.0;
|
var indent = 0.4 + node.depth * 1.0;
|
||||||
var expandable = node.isDir || node.isZip;
|
// Table-leaf dirs render like a file: no chevron, click opens the
|
||||||
|
// table in the preview pane (handled by events.js / preview.js).
|
||||||
|
var tableLeaf = window.app.modules.util.isTableLeaf(node);
|
||||||
|
var expandable = (node.isDir || node.isZip) && !tableLeaf;
|
||||||
var iconChar = iconForNode(node);
|
var iconChar = iconForNode(node);
|
||||||
var chevronClass = 'tree-name__chevron'
|
var chevronClass = 'tree-name__chevron'
|
||||||
+ (expandable ? '' : ' tree-name__chevron--leaf');
|
+ (expandable ? '' : ' tree-name__chevron--leaf');
|
||||||
|
|
@ -385,6 +395,7 @@
|
||||||
+ '" data-id="' + node.id
|
+ '" data-id="' + node.id
|
||||||
+ '" data-isdir="' + node.isDir
|
+ '" data-isdir="' + node.isDir
|
||||||
+ '" data-iszip="' + node.isZip + '"'
|
+ '" data-iszip="' + node.isZip + '"'
|
||||||
|
+ (tableLeaf ? ' data-tableleaf="true"' : '')
|
||||||
+ (node.virtual ? ' data-virtual="true"' : '')
|
+ (node.virtual ? ' data-virtual="true"' : '')
|
||||||
+ ' style="padding-left:' + indent + 'rem"'
|
+ ' style="padding-left:' + indent + 'rem"'
|
||||||
+ ' role="treeitem" tabindex="-1">'
|
+ ' role="treeitem" tabindex="-1">'
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,17 @@
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isTableLeaf reports whether a directory node should behave as a
|
||||||
|
// click-to-table LEAF rather than an expandable folder — i.e. the
|
||||||
|
// cascade resolved its default tool to "tables" (mdl/rsk/ssr and any
|
||||||
|
// operator-configured table dir). The tree renders it without a
|
||||||
|
// chevron and the preview pane opens the tables tool for it. Server
|
||||||
|
// mode only: defaultTool is a server-computed listing hint, absent
|
||||||
|
// offline (file:// folders stay ordinary expandable dirs).
|
||||||
|
function isTableLeaf(node) {
|
||||||
|
return !!(node && node.isDir && node.defaultTool === 'tables');
|
||||||
|
}
|
||||||
|
|
||||||
window.app.modules.util = {
|
window.app.modules.util = {
|
||||||
escapeHtml: escapeHtml,
|
escapeHtml: escapeHtml,
|
||||||
hashContent: hashContent,
|
hashContent: hashContent,
|
||||||
|
|
@ -207,6 +218,7 @@
|
||||||
fmtSize: fmtSize,
|
fmtSize: fmtSize,
|
||||||
isZipMemberNode: isZipMemberNode,
|
isZipMemberNode: isZipMemberNode,
|
||||||
isEditableZipMember: isEditableZipMember,
|
isEditableZipMember: isEditableZipMember,
|
||||||
|
isTableLeaf: isTableLeaf,
|
||||||
saveFile: saveFile,
|
saveFile: saveFile,
|
||||||
saveCopy: saveCopy,
|
saveCopy: saveCopy,
|
||||||
ConflictError: ConflictError
|
ConflictError: ConflictError
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,10 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
||||||
Declared: declared,
|
Declared: declared,
|
||||||
Title: title,
|
Title: title,
|
||||||
Verbs: subVerbs.String(),
|
Verbs: subVerbs.String(),
|
||||||
|
// Cascade-resolved default tool for this child dir, so the
|
||||||
|
// browse client can render a tool-typed dir (e.g. tables) as
|
||||||
|
// a click-to-open leaf without re-walking the cascade.
|
||||||
|
DefaultTool: zddc.DefaultToolAt(fsRoot, subAbs),
|
||||||
}
|
}
|
||||||
result = append(result, fi)
|
result = append(result, fi)
|
||||||
continue
|
continue
|
||||||
|
|
@ -384,6 +388,9 @@ func virtualCanonicalFolders(ctx context.Context, decider policy.Decider, fsRoot
|
||||||
DisplayName: lookupDisplay(displayMap, name),
|
DisplayName: lookupDisplay(displayMap, name),
|
||||||
Declared: true, // synthesized entries are by definition cascade-declared
|
Declared: true, // synthesized entries are by definition cascade-declared
|
||||||
Verbs: verbs.String(),
|
Verbs: verbs.String(),
|
||||||
|
// Cascade default tool for this virtual peer — mdl/rsk/ssr resolve
|
||||||
|
// to "tables", which browse renders as a click-to-table leaf.
|
||||||
|
DefaultTool: zddc.DefaultToolAt(fsRoot, childAbs),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return synth
|
return synth
|
||||||
|
|
|
||||||
|
|
@ -94,4 +94,12 @@ type FileInfo struct {
|
||||||
// where they apply. False/absent for directories, virtual entries,
|
// where they apply. False/absent for directories, virtual entries,
|
||||||
// and files outside a history-enabled subtree.
|
// and files outside a history-enabled subtree.
|
||||||
History bool `json:"history,omitempty"`
|
History bool `json:"history,omitempty"`
|
||||||
|
|
||||||
|
// DefaultTool is the cascade-resolved default tool for a DIRECTORY
|
||||||
|
// entry (the tool served at <dir> with no trailing slash) — e.g.
|
||||||
|
// "tables", "classifier", "browse". Empty for files and for dirs with
|
||||||
|
// no declared default. Browse uses it to render a tool-typed dir as a
|
||||||
|
// leaf that opens the tool in the preview pane instead of expanding —
|
||||||
|
// e.g. mdl/rsk/ssr (default_tool=tables) become click-to-table entries.
|
||||||
|
DefaultTool string `json:"default_tool,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue