feat(browse): Export context-menu submenu (folder→.zip, file→other formats)
Add an "Export" item to the row context menu with a submenu: - a folder offers ".zip" (reuses download.downloadFolder; works offline + server) - an md/docx/html file offers the OTHER two formats, each triggering a server-side conversion download via the new download.exportFile (builds the sibling-extension URL and lets the browser pull the converted bytes). File conversion is server-only, so it's hidden in offline (FS) mode; a zip is already an archive and gets no Export. menu-model's toMenuItem now passes a descriptor's `items` through as a submenu (resolved against the captured browse ctx) instead of only emitting action rows. Verified: 11/11 browse Playwright specs pass (incl. menu/context + Download ZIP); a logic harness confirms the per-type submenu contents and that clicks route to download.exportFile / downloadFolder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
16d88010a6
commit
382645b2d2
2 changed files with 68 additions and 4 deletions
|
|
@ -183,8 +183,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Export a file converted to another format. Server-only: builds the
|
||||
// sibling-extension URL (foo.docx → foo.md) and lets the browser pull it —
|
||||
// zddc-server recognises the virtual path and converts on the fly, emitting
|
||||
// Content-Disposition. fmt is a bare extension ("md" | "docx" | "html").
|
||||
function exportFile(node, fmt) {
|
||||
if (!node || node.isDir) {
|
||||
events().statusError('Not a file: ' + (node && node.name));
|
||||
return;
|
||||
}
|
||||
if (state.source !== 'server') {
|
||||
events().statusError('Export to .' + fmt + ' needs a server connection');
|
||||
return;
|
||||
}
|
||||
var tree = window.app.modules.tree;
|
||||
var path = tree && tree.pathFor ? tree.pathFor(node) : node.url;
|
||||
if (!path) {
|
||||
events().statusError('No path for ' + node.name);
|
||||
return;
|
||||
}
|
||||
var url = path.replace(/\.[^./]+$/, '') + '.' + fmt;
|
||||
var name = node.name.replace(/\.[^./]+$/, '') + '.' + fmt;
|
||||
events().statusInfo('Exporting ' + name + '…');
|
||||
downloadUrl(name, url);
|
||||
setTimeout(function () { events().statusClear(); }, 2500);
|
||||
}
|
||||
|
||||
window.app.modules.download = {
|
||||
downloadFile: downloadFile,
|
||||
downloadFolder: downloadFolder
|
||||
downloadFolder: downloadFolder,
|
||||
exportFile: exportFile
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@
|
|||
function isServer() { return state.source === 'server'; }
|
||||
function appliesToFolderLike(node) { return !!(node && (node.isDir || node.isZip)); }
|
||||
function appliesToFile(node) { return !!(node && !node.isDir && !node.isZip); }
|
||||
|
||||
// Formats the Export submenu offers for a file (server-side conversion):
|
||||
// a file of one of these extensions can be exported as the other two.
|
||||
var EXPORT_FORMATS = ['md', 'docx', 'html'];
|
||||
function cap() { return window.zddc && window.zddc.cap; }
|
||||
|
||||
function canVerb(node, verb) {
|
||||
|
|
@ -176,6 +180,32 @@
|
|||
else d.downloadFile(ctx.node);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Export submenu: a folder offers ".zip" (both modes); a md/docx/html
|
||||
// file offers the OTHER two formats (server-side conversion, so
|
||||
// server mode only). A zip is already an archive — no Export.
|
||||
id: 'export', group: 'io', surfaces: ['row'],
|
||||
label: 'Export',
|
||||
appliesTo: function (ctx) {
|
||||
var n = ctx.node;
|
||||
if (!n || n.virtual) return false;
|
||||
if (n.isDir) return true;
|
||||
if (n.isZip) return false;
|
||||
return isServer() && EXPORT_FORMATS.indexOf((n.ext || '').toLowerCase()) !== -1;
|
||||
},
|
||||
items: function (ctx) {
|
||||
var n = ctx.node;
|
||||
var d = window.app.modules.download;
|
||||
if (!d) return [];
|
||||
if (n.isDir) {
|
||||
return [{ label: '.zip', action: function () { d.downloadFolder(n); } }];
|
||||
}
|
||||
var cur = (n.ext || '').toLowerCase();
|
||||
return EXPORT_FORMATS.filter(function (f) { return f !== cur; }).map(function (fmt) {
|
||||
return { label: '.' + fmt, action: function () { d.exportFile(n, fmt); } };
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ── create (folder rows + pane; NOT file rows) ──
|
||||
// Create actions are HIDDEN unless the user can create here (the
|
||||
|
|
@ -361,7 +391,7 @@
|
|||
}
|
||||
|
||||
function toMenuItem(d, ctx) {
|
||||
return {
|
||||
var item = {
|
||||
label: resolve(d.label, ctx),
|
||||
accel: d.accel,
|
||||
danger: d.danger,
|
||||
|
|
@ -370,9 +400,16 @@
|
|||
disabled: function () { return !resolveBool(d.enabled, ctx, true); },
|
||||
tooltip: function () {
|
||||
return resolveBool(d.enabled, ctx, true) ? '' : (resolve(d.tooltip, ctx) || '');
|
||||
},
|
||||
action: function () { if (d.action) d.action(ctx); }
|
||||
}
|
||||
};
|
||||
// A descriptor with `items` becomes a submenu (resolved against the
|
||||
// captured browse ctx); otherwise it's a normal action row.
|
||||
if (d.items) {
|
||||
item.items = function () { return resolve(d.items, ctx); };
|
||||
} else {
|
||||
item.action = function () { if (d.action) d.action(ctx); };
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function project(surface, ctx) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue