|
|
|
|
@ -2476,7 +2476,7 @@ body {
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="header-title-group">
|
|
|
|
|
<span class="app-header__title">ZDDC Browse</span>
|
|
|
|
|
<span class="build-timestamp">v0.0.25</span>
|
|
|
|
|
<span class="build-timestamp">v0.0.26</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
|
|
|
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
|
|
|
|
@ -8533,6 +8533,10 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
|
|
|
|
|
async function saveContent(node, content) {
|
|
|
|
|
if (node.handle && typeof node.handle.createWritable === 'function') {
|
|
|
|
|
// Local folders are picked read-only; escalate to readwrite on
|
|
|
|
|
// first save (one FS-Access prompt, then granted for the session).
|
|
|
|
|
var up = window.app.modules.upload;
|
|
|
|
|
if (up && up.ensureWritable) await up.ensureWritable();
|
|
|
|
|
var writable = await node.handle.createWritable();
|
|
|
|
|
await writable.write(content);
|
|
|
|
|
await writable.close();
|
|
|
|
|
@ -10258,13 +10262,48 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
} catch (_e) { /* refresh is best-effort */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Write-permission escalation (FS-API mode) ──────────────────────────
|
|
|
|
|
// The local folder is picked read-only (showDirectoryPicker mode:read)
|
|
|
|
|
// so browsing never prompts. The first mutation escalates to readwrite
|
|
|
|
|
// via the FS-Access permission prompt; granting on the picked root
|
|
|
|
|
// covers every descendant handle. Must run under a user gesture — every
|
|
|
|
|
// caller is reached from a click/menu action. No-op in server mode or
|
|
|
|
|
// on browsers without the permission API.
|
|
|
|
|
async function ensureWritable() {
|
|
|
|
|
if (state.source !== 'fs') return;
|
|
|
|
|
var root = state.rootHandle;
|
|
|
|
|
if (!root || typeof root.requestPermission !== 'function') return;
|
|
|
|
|
var opts = { mode: 'readwrite' };
|
|
|
|
|
if ((await root.queryPermission(opts)) === 'granted') return;
|
|
|
|
|
if ((await root.requestPermission(opts)) === 'granted') return;
|
|
|
|
|
throw new Error('Write permission denied — grant edit access to the folder when prompted.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleForDir resolves a directory PATH (FS-API mode) to its
|
|
|
|
|
// FileSystemDirectoryHandle: the picked root for the current scope,
|
|
|
|
|
// else the matching expanded node's handle. Returns null if unknown.
|
|
|
|
|
function handleForDir(dirPath) {
|
|
|
|
|
var tree = window.app.modules.tree;
|
|
|
|
|
if (!dirPath.endsWith('/')) dirPath += '/';
|
|
|
|
|
if (dirPath === state.currentPath) return state.rootHandle;
|
|
|
|
|
var noSlash = dirPath.replace(/\/$/, '');
|
|
|
|
|
var hit = null;
|
|
|
|
|
state.nodes.forEach(function (n) {
|
|
|
|
|
if (hit || !n.isDir || !n.handle) return;
|
|
|
|
|
if (tree && tree.pathFor(n).replace(/\/$/, '') === noSlash) hit = n;
|
|
|
|
|
});
|
|
|
|
|
return hit ? hit.handle : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Create-new helpers ────────────────────────────────────────────────
|
|
|
|
|
// Both go through the same server endpoints used by upload: PUT
|
|
|
|
|
// for files (with an empty/template body) and POST + X-ZDDC-Op:
|
|
|
|
|
// mkdir for directories. Client-side enforcement is best-effort;
|
|
|
|
|
// the server's ACL is the source of truth.
|
|
|
|
|
// Server mode: PUT for files (empty/template body) and POST +
|
|
|
|
|
// X-ZDDC-Op: mkdir for directories; the server's ACL is the source of
|
|
|
|
|
// truth. FS-API mode: create directly in the picked tree via
|
|
|
|
|
// getDirectoryHandle/getFileHandle({create:true}) + createWritable —
|
|
|
|
|
// limited only by the filesystem permission the user granted.
|
|
|
|
|
|
|
|
|
|
async function makeDir(parentDir, name) {
|
|
|
|
|
if (state.source === 'server') {
|
|
|
|
|
var url = joinUrl(parentDir, name);
|
|
|
|
|
if (!url.endsWith('/')) url += '/';
|
|
|
|
|
var resp = await fetch(url, {
|
|
|
|
|
@ -10273,9 +10312,16 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
headers: { 'X-ZDDC-Op': 'mkdir' }
|
|
|
|
|
});
|
|
|
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var parent = handleForDir(parentDir);
|
|
|
|
|
if (!parent) throw new Error('No directory handle for ' + parentDir);
|
|
|
|
|
await ensureWritable();
|
|
|
|
|
await parent.getDirectoryHandle(name, { create: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function makeFile(parentDir, name, body, contentType) {
|
|
|
|
|
if (state.source === 'server') {
|
|
|
|
|
var resp = await fetch(joinUrl(parentDir, name), {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
credentials: 'same-origin',
|
|
|
|
|
@ -10283,6 +10329,15 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
body: body == null ? '' : body
|
|
|
|
|
});
|
|
|
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var parent = handleForDir(parentDir);
|
|
|
|
|
if (!parent) throw new Error('No directory handle for ' + parentDir);
|
|
|
|
|
await ensureWritable();
|
|
|
|
|
var fh = await parent.getFileHandle(name, { create: true });
|
|
|
|
|
var w = await fh.createWritable();
|
|
|
|
|
await w.write(body == null ? '' : body);
|
|
|
|
|
await w.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Delete + rename ─────────────────────────────────────────────────────
|
|
|
|
|
@ -10347,6 +10402,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
// legacy removeEntry() lives on the PARENT directory handle
|
|
|
|
|
// and we don't retain ancestor handles).
|
|
|
|
|
if (node.handle && typeof node.handle.remove === 'function') {
|
|
|
|
|
await ensureWritable();
|
|
|
|
|
await node.handle.remove({ recursive: !!node.isDir });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@ -10387,6 +10443,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
}
|
|
|
|
|
// FS-API: handle.move(newName) is Chromium 110+.
|
|
|
|
|
if (node.handle && typeof node.handle.move === 'function') {
|
|
|
|
|
await ensureWritable();
|
|
|
|
|
await node.handle.move(newName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@ -10501,6 +10558,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
removeNode: removeNode,
|
|
|
|
|
renameNode: renameNode,
|
|
|
|
|
canMutate: canMutate,
|
|
|
|
|
ensureWritable: ensureWritable,
|
|
|
|
|
UPLOAD_MAX_BYTES: UPLOAD_MAX_BYTES
|
|
|
|
|
};
|
|
|
|
|
})();
|
|
|
|
|
@ -12828,9 +12886,187 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
return parentDir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(s) {
|
|
|
|
|
return String(s).replace(/[&<>"']/g, function (c) {
|
|
|
|
|
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Valid party folder name — mirrors zddc.ValidPartyName server-side
|
|
|
|
|
// (^[A-Za-z0-9][A-Za-z0-9.-]*$).
|
|
|
|
|
function validPartyName(s) { return /^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(s || ''); }
|
|
|
|
|
|
|
|
|
|
// The project-level folder-nav aggregators. These have no physical
|
|
|
|
|
// presence: <project>/<slot>/ lists the parties whose
|
|
|
|
|
// archive/<party>/<slot>/ has content. Creating something here means
|
|
|
|
|
// creating it under a party — see createInAggregator.
|
|
|
|
|
var FOLDER_NAV_SLOTS = { working: 1, staging: 1, reviewing: 1 };
|
|
|
|
|
|
|
|
|
|
// aggregatorRoot returns { project, slot } when parentDir is a
|
|
|
|
|
// project-level folder-nav aggregator root (server mode only), else
|
|
|
|
|
// null. parentDir is a "/<project>/<slot>/" URL.
|
|
|
|
|
function aggregatorRoot(parentDir) {
|
|
|
|
|
if (state.source !== 'server') return null;
|
|
|
|
|
var segs = (parentDir || '').replace(/^\/+|\/+$/g, '').split('/');
|
|
|
|
|
if (segs.length !== 2 || !segs[0]) return null;
|
|
|
|
|
var slot = segs[1].toLowerCase();
|
|
|
|
|
return FOLDER_NAV_SLOTS[slot] ? { project: segs[0], slot: slot } : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rewriteAggregatorPath maps a path UNDER a folder-nav aggregator
|
|
|
|
|
// (a party already chosen — /<project>/<slot>/<party>[/<rest>]) to its
|
|
|
|
|
// canonical archive path /<project>/archive/<party>/<slot>[/<rest>],
|
|
|
|
|
// mirroring the server's folder-nav redirect. Returns null when
|
|
|
|
|
// parentDir isn't under such an aggregator (root case is handled by
|
|
|
|
|
// aggregatorRoot + the picker). Covers right-clicking a party row
|
|
|
|
|
// shown in an aggregator listing so "New folder" doesn't 409.
|
|
|
|
|
function rewriteAggregatorPath(parentDir) {
|
|
|
|
|
if (state.source !== 'server') return null;
|
|
|
|
|
var segs = (parentDir || '').replace(/^\/+|\/+$/g, '').split('/');
|
|
|
|
|
if (segs.length < 3 || !segs[0]) return null;
|
|
|
|
|
var slot = segs[1].toLowerCase();
|
|
|
|
|
if (!FOLDER_NAV_SLOTS[slot]) return null;
|
|
|
|
|
var p = '/' + segs[0] + '/archive/' + segs[2] + '/' + slot + '/';
|
|
|
|
|
var rest = segs.slice(3);
|
|
|
|
|
if (rest.length) p += rest.join('/') + '/';
|
|
|
|
|
return p;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// List the parties under a project's archive/ (folder names), sorted.
|
|
|
|
|
async function fetchParties(project) {
|
|
|
|
|
try {
|
|
|
|
|
var entries = await loader.fetchServerChildren('/' + project + '/archive/');
|
|
|
|
|
return entries
|
|
|
|
|
.filter(function (e) { return e.isDir; })
|
|
|
|
|
.map(function (e) { return e.name; })
|
|
|
|
|
.sort(function (a, b) { return a.localeCompare(b); });
|
|
|
|
|
} catch (_e) { return []; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// openPartyPicker resolves to { party, name } once the user picks a
|
|
|
|
|
// party (existing or new) and a name, or null on cancel. Mirrors the
|
|
|
|
|
// stage.js modal styling. New-party creation is offered but the server
|
|
|
|
|
// gates it to the document_controller (a 403 surfaces a clear message).
|
|
|
|
|
function openPartyPicker(opts) {
|
|
|
|
|
return new Promise(function (resolve) {
|
|
|
|
|
var kindWord = opts.kind === 'folder' ? 'folder' : 'file';
|
|
|
|
|
var overlay = document.createElement('div');
|
|
|
|
|
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:9000;';
|
|
|
|
|
var box = document.createElement('div');
|
|
|
|
|
box.style.cssText = 'background:var(--bg,#fff);color:var(--fg,#111);padding:1.25rem 1.5rem;border-radius:6px;min-width:28rem;max-width:36rem;box-shadow:0 4px 20px rgba(0,0,0,0.25);';
|
|
|
|
|
|
|
|
|
|
var partyList = opts.parties.map(function (name) {
|
|
|
|
|
return '<label style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;cursor:pointer;">' +
|
|
|
|
|
'<input type="radio" name="pp-party" value="' + escapeHtml(name) + '">' +
|
|
|
|
|
'<span style="font-family:var(--code,monospace);">' + escapeHtml(name) + '</span></label>';
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
box.innerHTML =
|
|
|
|
|
'<h2 style="margin:0 0 0.5rem 0;font-size:1.1rem;">New ' + kindWord + ' in ' + escapeHtml(opts.slot) + '/</h2>' +
|
|
|
|
|
'<p style="margin:0 0 0.5rem 0;font-size:0.85rem;color:#666;">' +
|
|
|
|
|
escapeHtml(opts.slot) + '/ aggregates each party’s work, so it has no folder of its own. ' +
|
|
|
|
|
'Pick the party this ' + kindWord + ' belongs to — it lands under <code>archive/<party>/' + escapeHtml(opts.slot) + '/</code>.' +
|
|
|
|
|
'</p>' +
|
|
|
|
|
'<div style="max-height:14rem;overflow-y:auto;border:1px solid rgba(0,0,0,0.1);padding:0.3rem 0.6rem;margin-bottom:0.5rem;">' +
|
|
|
|
|
(partyList || '<em style="color:#888;">No parties yet — create one below.</em>') +
|
|
|
|
|
'<label style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;cursor:pointer;border-top:1px solid rgba(0,0,0,0.05);margin-top:0.3rem;padding-top:0.5rem;">' +
|
|
|
|
|
'<input type="radio" name="pp-party" value="__new__">' +
|
|
|
|
|
'<span><strong>+ New party…</strong> <span style="color:#888;font-size:0.8rem;">(document controller only)</span></span></label>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<div id="pp-newparty-row" style="display:none;margin-bottom:0.5rem;font-size:0.9rem;">' +
|
|
|
|
|
'<label for="pp-newparty">New party name</label><br>' +
|
|
|
|
|
'<input id="pp-newparty" type="text" style="width:100%;padding:0.3rem;font-family:var(--code,monospace);" placeholder="Acme">' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<label for="pp-name" style="font-size:0.9rem;">' + (opts.kind === 'folder' ? 'Folder' : 'File') + ' name</label>' +
|
|
|
|
|
'<input id="pp-name" type="text" style="width:100%;padding:0.3rem;font-family:var(--code,monospace);" value="' + (opts.kind === 'folder' ? 'new-folder' : 'new.md') + '">' +
|
|
|
|
|
'<div style="display:flex;justify-content:flex-end;gap:0.5rem;margin-top:1rem;">' +
|
|
|
|
|
'<button type="button" id="pp-cancel">Cancel</button>' +
|
|
|
|
|
'<button type="button" id="pp-submit" class="btn-primary">Create</button>' +
|
|
|
|
|
'</div>';
|
|
|
|
|
overlay.appendChild(box);
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
|
|
|
|
|
var newRow = box.querySelector('#pp-newparty-row');
|
|
|
|
|
var newInput = box.querySelector('#pp-newparty');
|
|
|
|
|
box.querySelectorAll('input[name="pp-party"]').forEach(function (r) {
|
|
|
|
|
r.addEventListener('change', function () {
|
|
|
|
|
var isNew = (r.value === '__new__' && r.checked);
|
|
|
|
|
newRow.style.display = isNew ? '' : 'none';
|
|
|
|
|
if (isNew) newInput.focus();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function close() { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); }
|
|
|
|
|
function cancel() { close(); resolve(null); }
|
|
|
|
|
box.querySelector('#pp-cancel').addEventListener('click', cancel);
|
|
|
|
|
overlay.addEventListener('click', function (e) { if (e.target === overlay) cancel(); });
|
|
|
|
|
box.querySelector('#pp-submit').addEventListener('click', function () {
|
|
|
|
|
var sel = box.querySelector('input[name="pp-party"]:checked');
|
|
|
|
|
if (!sel) { statusError('Pick a party.'); return; }
|
|
|
|
|
var party;
|
|
|
|
|
if (sel.value === '__new__') {
|
|
|
|
|
party = newInput.value.trim();
|
|
|
|
|
if (!validPartyName(party)) {
|
|
|
|
|
statusError('Party name: a letter or digit, then letters/digits/dot/hyphen.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
party = sel.value;
|
|
|
|
|
}
|
|
|
|
|
var nv = validateName(box.querySelector('#pp-name').value);
|
|
|
|
|
if (!nv.ok) { statusError(nv.msg); return; }
|
|
|
|
|
close();
|
|
|
|
|
resolve({ party: party, name: nv.name });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createInAggregator routes a New folder/file in a virtual aggregator
|
|
|
|
|
// root to archive/<party>/<slot>/<name> after prompting for the party.
|
|
|
|
|
async function createInAggregator(agg, kind) {
|
|
|
|
|
var up = window.app.modules.upload;
|
|
|
|
|
if (!up) return;
|
|
|
|
|
var parties = await fetchParties(agg.project);
|
|
|
|
|
var choice = await openPartyPicker({ project: agg.project, slot: agg.slot, kind: kind, parties: parties });
|
|
|
|
|
if (!choice) return;
|
|
|
|
|
// Party names are validated to a URL-safe charset, so no encoding
|
|
|
|
|
// needed for the party segment; makeDir/makeFile encode the leaf.
|
|
|
|
|
var targetDir = '/' + agg.project + '/archive/' + choice.party + '/' + agg.slot + '/';
|
|
|
|
|
try {
|
|
|
|
|
if (kind === 'folder') {
|
|
|
|
|
await up.makeDir(targetDir, choice.name);
|
|
|
|
|
statusInfo('Created ' + choice.party + '/' + agg.slot + '/' + choice.name);
|
|
|
|
|
} else {
|
|
|
|
|
var name = /\.(md|markdown)$/i.test(choice.name) ? choice.name : choice.name + '.md';
|
|
|
|
|
var template = '# ' + name.replace(/\.(md|markdown)$/i, '') + '\n\n';
|
|
|
|
|
await up.makeFile(targetDir, name, template, 'text/markdown; charset=utf-8');
|
|
|
|
|
statusInfo('Created ' + choice.party + '/' + agg.slot + '/' + name);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
var msg = (e && e.message) || String(e);
|
|
|
|
|
if (/\b403\b/.test(msg)) {
|
|
|
|
|
statusError('Not allowed — creating a new party requires the document-controller role.');
|
|
|
|
|
} else {
|
|
|
|
|
statusError('Create failed: ' + msg);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Refresh the aggregator view — the party now appears if it had no
|
|
|
|
|
// content before.
|
|
|
|
|
await reloadDir('/' + agg.project + '/' + agg.slot + '/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createInDir(parentDir, kind) {
|
|
|
|
|
var up = window.app.modules.upload;
|
|
|
|
|
if (!up) return;
|
|
|
|
|
// A project-level folder-nav aggregator (working/staging/reviewing)
|
|
|
|
|
// has no physical home — route through the party picker instead of
|
|
|
|
|
// erroring on an unplaceable mkdir/PUT.
|
|
|
|
|
var agg = aggregatorRoot(parentDir);
|
|
|
|
|
if (agg) return createInAggregator(agg, kind);
|
|
|
|
|
// A party already chosen inside an aggregator view → canonical path.
|
|
|
|
|
var rewritten = rewriteAggregatorPath(parentDir);
|
|
|
|
|
if (rewritten) parentDir = rewritten;
|
|
|
|
|
var promptMsg = kind === 'folder'
|
|
|
|
|
? 'New folder name (under ' + parentDir + '):'
|
|
|
|
|
: 'New markdown filename (under ' + parentDir + '):';
|
|
|
|
|
@ -12996,6 +13232,13 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
// Items are kept VISIBLE but DISABLED when they don't apply, so
|
|
|
|
|
// every menu has the same shape regardless of what the user
|
|
|
|
|
// right-clicked. Predictable position = muscle memory.
|
|
|
|
|
// canCreateHere — whether New folder/file has a writable target: the
|
|
|
|
|
// server (ACL decides the rest) or a picked local folder (the
|
|
|
|
|
// filesystem permission decides, escalated on first write).
|
|
|
|
|
function canCreateHere() {
|
|
|
|
|
return state.source === 'server' || (state.source === 'fs' && !!state.rootHandle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildTreeRowMenu(ctx) {
|
|
|
|
|
var serverMode = state.source === 'server';
|
|
|
|
|
var canMutate = function (c) {
|
|
|
|
|
@ -13055,12 +13298,12 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
// ── Create new (in the row's parent folder) ──
|
|
|
|
|
{
|
|
|
|
|
label: 'New folder',
|
|
|
|
|
disabled: !serverMode,
|
|
|
|
|
disabled: !canCreateHere(),
|
|
|
|
|
action: function (c) { createInside(c.node, 'folder'); }
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'New markdown file',
|
|
|
|
|
disabled: !serverMode,
|
|
|
|
|
disabled: !canCreateHere(),
|
|
|
|
|
action: function (c) { createInside(c.node, 'markdown'); }
|
|
|
|
|
},
|
|
|
|
|
{ separator: true },
|
|
|
|
|
@ -13269,12 +13512,12 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
label: 'New folder',
|
|
|
|
|
disabled: !serverMode,
|
|
|
|
|
disabled: !canCreateHere(),
|
|
|
|
|
action: function () { createInDir(state.currentPath || '/', 'folder'); }
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'New markdown file',
|
|
|
|
|
disabled: !serverMode,
|
|
|
|
|
disabled: !canCreateHere(),
|
|
|
|
|
action: function () { createInDir(state.currentPath || '/', 'markdown'); }
|
|
|
|
|
},
|
|
|
|
|
// ── Create Transmittal folder (staging/ scope only) ──
|
|
|
|
|
|