收口工作台资源 tab 与本地文件树 P1

This commit is contained in:
lix-2026
2026-05-20 21:45:11 +08:00
parent fe13444dbc
commit a29d9868f6
13 changed files with 1595 additions and 214 deletions
+260 -7
View File
@@ -1832,6 +1832,14 @@ const SIDEBAR_TREE_JS: &str = r##"
return String(item && (item.iconHint || item.rowKind || 'file') || 'file').trim() || 'file';
}
function fileCapabilitiesAttr(item) {
try {
return JSON.stringify(Array.isArray(item && item.capabilities) ? item.capabilities : []);
} catch (_error) {
return '[]';
}
}
function isFileTreeProjectionPageRow(rowKind, assetId) {
if (assetId) return false;
return rowKind === 'document' || rowKind === 'doc' || rowKind === 'markdown';
@@ -1873,7 +1881,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var childHtml = expandable
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '</ul>'
: '';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作"></button></div></div>' + childHtml + '</li>';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-capabilities="' + escapeHtml(fileCapabilitiesAttr(item)) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作"></button></div></div>' + childHtml + '</li>';
}).join('');
}
@@ -2214,11 +2222,40 @@ const SIDEBAR_TREE_JS: &str = r##"
async function openConvexAssetFromFileTree(detail) {
var assetId = String(detail && detail.assetId || '').trim();
if (!assetId) return;
var forceNewWindow = String(detail && detail.openTarget || '').trim() === 'new-window';
var openTarget = String(detail && detail.openTarget || '').trim().toLowerCase();
var forceNewWindow = openTarget === 'new-window';
if (openTarget === 'side') {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceAsSideTarget === 'function') {
void window.__mnoteDocumentPaneRuntime.openResourceAsSideTarget({
objectIdentity: String(detail.objectIdentity || detail.assetId || ''),
assetId: assetId,
title: String(detail.title || detail.fileName || assetId || ''),
kind: String(detail.iconKind || detail.assetType || 'file'),
documentId: String(detail.documentId || currentDocumentId() || ''),
workspaceId: String(detail.workspaceId || resolveWorkspaceId(document.body) || '')
});
}
return;
}
var localFilePath = localFilePathFromAssetId(assetId);
if (localFilePath) {
var localFileName = localFilePath.split('/').pop() || localFilePath;
if (isMindmapAssetDetail(detail) || String(detail && detail.assetType || '').trim() === 'mindmap') {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-resource-tab');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: 'resource:mindmap:' + String(detail && detail.documentId || currentDocumentId() || '').trim() + ':' + assetId,
assetId: assetId,
mindmapId: assetId,
title: String(detail && detail.title || localFileName || '').trim(),
fileName: localFileName,
kind: 'mindmap',
documentId: String(detail && detail.documentId || currentDocumentId() || '').trim(),
workspaceId: String(detail.workspaceId || '').trim()
});
return;
}
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'local-mindmap-object-shell');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
navigateToMindmapObject(String(detail && detail.documentId || currentDocumentId() || '').trim(), assetId, String(detail.workspaceId || '').trim());
@@ -2255,6 +2292,21 @@ const SIDEBAR_TREE_JS: &str = r##"
}
var documentId = String(detail && detail.documentId || '').trim();
if (isMindmapAssetDetail(detail) && documentId) {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-resource-tab');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: 'resource:mindmap:' + documentId + ':' + assetId,
assetId: assetId,
mindmapId: assetId,
title: String(detail.title || '').trim(),
fileName: String(detail.title || '').trim(),
kind: 'mindmap',
documentId: documentId,
workspaceId: String(detail.workspaceId || '').trim()
});
return;
}
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-object-shell');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
navigateToMindmapObject(documentId, assetId, String(detail.workspaceId || '').trim());
@@ -2324,6 +2376,130 @@ const SIDEBAR_TREE_JS: &str = r##"
});
}
function fileTreeRowCapabilities(row) {
if (!(row instanceof HTMLElement)) return [];
try {
var parsed = JSON.parse(row.getAttribute('data-capabilities') || '[]');
return Array.isArray(parsed) ? parsed.map(function(item) { return String(item || '').trim(); }).filter(Boolean) : [];
} catch (_) {
return [];
}
}
function fileTreeRowIsReadonly(row) {
return fileTreeRowCapabilities(row).some(function(capability) {
return ['readonly', 'readOnly', 'permissionDenied'].indexOf(capability) >= 0;
});
}
function blockReadonlyFileTreeAction(action, detail, message) {
var normalizedAction = String(action || 'drop').trim() || 'drop';
var text = String(message || '').trim();
var targetRowId = String(detail && (detail.targetRowId || detail.rowId) || '').trim();
var documentId = String(detail && detail.documentId || '').trim();
var assetId = String(detail && detail.assetId || '').trim();
recordFileTreeAction(normalizedAction, {
rowId: targetRowId,
documentId: documentId,
assetId: assetId,
readonly: true
});
recordFileTreeActionStatus('blocked', {
rowId: targetRowId,
documentId: documentId,
assetId: assetId,
readonly: true,
fallback: 'alert'
});
document.documentElement.setAttribute('data-mnote-filetree-readonly-blocked', normalizedAction);
document.documentElement.setAttribute('data-mnote-filetree-readonly-message', text);
if (targetRowId) {
document.documentElement.setAttribute('data-mnote-filetree-readonly-target-row-id', targetRowId);
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(targetRowId) + '"]');
if (row instanceof HTMLElement) {
row.setAttribute('data-readonly-blocked', normalizedAction);
row.setAttribute('data-readonly-message', text);
}
}
window.alert(text);
return false;
}
function fileTreeDocumentParentsForPreflight() {
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')).map(function(row) {
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
if (!documentId) return null;
var parentRowId = row.getAttribute('data-parent-id') || '';
var parentDocumentId = parentRowId.indexOf('doc:') === 0 ? parentRowId.slice(4) : parentRowId || null;
return { documentId: documentId, parentId: parentDocumentId };
}).filter(Boolean);
}
function fileTreeTargetChildrenForPreflight(targetRow) {
var node = targetRow instanceof HTMLElement ? targetRow.closest('.tree-node') : null;
if (!node) return [];
return Array.from(node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"]')).map(function(row) {
return {
rowKind: row.getAttribute('data-row-kind') || '',
documentId: row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || null,
assetId: row.getAttribute('data-asset-id') || null,
title: rowTitle(row)
};
});
}
function fileTreeDropPreflightRows() {
return fileTreeRowsForUploadPreflight().map(function(row) {
var domRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(row.rowId) + '"]');
row.title = rowTitle(domRow);
return row;
});
}
async function ensureFileTreeWritableTarget(action, targetRow, rowIds, copy) {
var normalizedAction = String(action || 'drop').trim() || 'drop';
if (!(targetRow instanceof HTMLElement)) return true;
var detail = {
targetRowId: targetRow.getAttribute('data-row-id') || '',
rowId: targetRow.getAttribute('data-row-id') || '',
documentId: targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') || '',
assetId: targetRow.getAttribute('data-asset-id') || ''
};
if (fileTreeRowIsReadonly(targetRow)) {
return blockReadonlyFileTreeAction(normalizedAction, detail, '');
}
try {
var response = await fetch('/api/tree/filetree/drop-preflight', {
method: 'POST',
credentials: 'include',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(targetRow),
copy: Boolean(copy),
sourceCapabilities: ['read', 'write', 'move'],
targetCapabilities: fileTreeRowCapabilities(targetRow),
targetDocumentId: detail.documentId || null,
targetRowId: detail.targetRowId || null,
focusedRowId: sidebarFileTreeSelection.focusedRowId || null,
activeDocumentId: currentDocumentId() || null,
rowIds: Array.isArray(rowIds) ? rowIds : [],
rows: fileTreeDropPreflightRows(),
targetChildren: fileTreeTargetChildrenForPreflight(targetRow),
documentParents: fileTreeDocumentParentsForPreflight()
})
});
if (response.ok) return true;
var payload = await response.json().catch(function() { return null; });
var message = payload && (payload.error || payload.message) ? String(payload.error || payload.message) : '';
if (message.indexOf('') >= 0 || message.toLowerCase().indexOf('readonly') >= 0) {
return blockReadonlyFileTreeAction(normalizedAction, detail, message);
}
return true;
} catch (_) {
return true;
}
}
function fileTreeDocumentWorkspacesForUploadPreflight(workspaceId) {
var seen = new Set();
return fileTreeRowsForUploadPreflight().filter(function(row) {
@@ -3327,6 +3503,28 @@ const SIDEBAR_TREE_JS: &str = r##"
return Promise.resolve();
}
function recordFileTreeAction(action, detail) {
var normalized = String(action || '').trim() || 'unknown';
var rowId = String(detail && detail.rowId || '').trim();
var documentId = String(detail && detail.documentId || '').trim();
var assetId = String(detail && detail.assetId || '').trim();
document.documentElement.setAttribute('data-mnote-filetree-last-action', normalized);
if (rowId) document.documentElement.setAttribute('data-mnote-filetree-last-action-row-id', rowId);
if (documentId) document.documentElement.setAttribute('data-mnote-filetree-last-action-document-id', documentId);
if (assetId) document.documentElement.setAttribute('data-mnote-filetree-last-action-asset-id', assetId);
window.dispatchEvent(new CustomEvent('tree.filetree.action', {
detail: Object.assign({}, detail || {}, { action: normalized })
}));
}
function recordFileTreeActionStatus(status, detail) {
var normalized = String(status || '').trim() || 'unknown';
document.documentElement.setAttribute('data-mnote-filetree-last-action-status', normalized);
window.dispatchEvent(new CustomEvent('tree.filetree.action.status', {
detail: Object.assign({}, detail || {}, { status: normalized })
}));
}
function documentHref(documentId, workspaceId) {
var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin);
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
@@ -3395,10 +3593,17 @@ const SIDEBAR_TREE_JS: &str = r##"
var title = detail.title || '';
var isAsset = detail.contextKind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
if (isAsset && action === 'new-window') {
recordFileTreeAction('new-window', detail);
void openConvexAssetFromFileTree({ ...detail, openTarget: 'new-window' });
return;
}
if (isAsset && action === 'open-right') {
recordFileTreeAction('open-right', detail);
void openConvexAssetFromFileTree({ ...detail, openTarget: 'side' });
return;
}
if (action === 'open-right') {
recordFileTreeAction('open-right', detail);
dispatchSidebarEvent('tree.page.open-right', detail);
return;
}
@@ -3428,10 +3633,14 @@ const SIDEBAR_TREE_JS: &str = r##"
}
if (action === 'delete-trash' && isAsset) {
if (!window.confirm('' + title + '')) return;
void deleteSingleFileTreeAsset(detail, trigger).catch(function(error) {
window.alert(error && error.message ? error.message : '');
}).then(function() {
recordFileTreeAction('delete-trash', detail);
recordFileTreeActionStatus('pending', detail);
void deleteSingleFileTreeAsset(detail, trigger).then(function() {
recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' }));
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
}).catch(function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '');
});
return;
}
@@ -3439,6 +3648,17 @@ const SIDEBAR_TREE_JS: &str = r##"
void createPage(trigger || document.body, documentId || null);
return;
}
if (action === 'paste-into') {
var pasteRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger;
recordFileTreeAction('paste-into', detail);
void pasteSidebarFileTreeClipboard(pasteRow).then(function(ok) {
recordFileTreeActionStatus(ok ? 'applied' : 'skipped', detail);
}).catch(function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '');
});
return;
}
if (action === 'copy-path') {
void copyTreeContextValue(title, 'copy-path');
return;
@@ -3496,13 +3716,17 @@ const SIDEBAR_TREE_JS: &str = r##"
var deleteTargetId = documentId || String(detail.rowId || '').trim();
if (action === 'delete-trash' && deleteTargetId) {
if (!window.confirm('' + title + '')) return;
recordFileTreeAction('delete-trash', detail);
recordFileTreeActionStatus('pending', detail);
void dispatchTreeCommand(trigger || document.body, {
action: 'archive',
workspaceId: workspaceId,
documentId: deleteTargetId
}).then(function() {
recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' }));
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
}).catch(function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '');
});
}
@@ -3594,7 +3818,7 @@ const SIDEBAR_TREE_JS: &str = r##"
{ separator: true },
{ action: 'new-file', icon: 'note_add', label: 'New File' },
{ action: 'new-folder', icon: 'create_new_folder', label: 'New Folder', disabled: true, title: 'Convex tree command' },
{ action: 'paste-into', icon: 'content_paste', label: 'Paste Into', disabled: true, title: '使 Ctrl/Cmd+V Paste Into selection target' },
{ action: 'paste-into', icon: 'content_paste', label: '', disabled: !sidebarFileTreeClipboard, title: sidebarFileTreeClipboard ? '' : '' },
{ action: 'refresh', icon: 'refresh', label: 'Refresh' },
{ action: 'collapse-all', icon: 'unfold_less', label: 'Collapse All' },
{ action: 'reveal', icon: 'my_location', label: 'Reveal' },
@@ -3886,11 +4110,20 @@ const SIDEBAR_TREE_JS: &str = r##"
var action = sidebarFileTreeClipboard.action === 'cut' ? 'move' : 'copy';
var rows = fileTreeRowsByRowIds(sidebarFileTreeClipboard.rowIds);
if (rows.length === 0) return false;
var writable = await ensureFileTreeWritableTarget('paste', targetRow, sidebarFileTreeClipboard.rowIds, action === 'copy');
if (!writable) return false;
recordFileTreeAction('paste', {
rowId: targetRow ? targetRow.getAttribute('data-row-id') || '' : '',
documentId: targetDocumentId,
sourceRowIds: sidebarFileTreeClipboard.rowIds,
clipboardAction: sidebarFileTreeClipboard.action
});
var plan = buildSidebarFileTreeDeletePlan(rows);
var workspaceId = resolveWorkspaceId(targetRow || document.body);
var failures = [];
if (action === 'copy') {
dispatchSidebarEvent('tree.filetree.copy-requested', { rowIds: sidebarFileTreeClipboard.rowIds, targetDocumentId: targetDocumentId });
recordFileTreeActionStatus('copy-requested', { documentId: targetDocumentId });
return true;
}
for (var i = 0; i < plan.docRows.length; i += 1) {
@@ -3922,10 +4155,12 @@ const SIDEBAR_TREE_JS: &str = r##"
}
}
if (failures.length > 0) {
recordFileTreeActionStatus('failed', { documentId: targetDocumentId, fallback: 'alert' });
window.alert('' + failures.join(''));
return false;
}
sidebarFileTreeClipboard = null;
recordFileTreeActionStatus('applied', { documentId: targetDocumentId });
return true;
}
@@ -3935,6 +4170,8 @@ const SIDEBAR_TREE_JS: &str = r##"
var total = plan.docRows.length + plan.folderRows.length + plan.fileAssetRows.length + plan.mindmapRows.length + plan.tableRows.length;
if (total === 0) return false;
if (!window.confirm(sidebarFileTreeDeleteConfirmText(plan))) return false;
recordFileTreeAction('bulk-delete', { rowId: trigger instanceof HTMLElement ? trigger.getAttribute('data-row-id') || '' : '', count: total });
recordFileTreeActionStatus('pending', { count: total });
var failures = [];
for (var i = 0; i < plan.docRows.length; i += 1) {
var docRow = plan.docRows[i];
@@ -4026,10 +4263,12 @@ const SIDEBAR_TREE_JS: &str = r##"
sidebarFileTreeSelection.focusedRowId = null;
syncSidebarFileTreeSelection();
if (failures.length > 0) {
recordFileTreeActionStatus('failed', { count: total, failures: failures.slice(0, 20), fallback: 'alert' });
window.alert('' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : ''));
return false;
}
document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true');
recordFileTreeActionStatus('archived', { count: total, undo: 'trash-modal' });
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
return true;
}
@@ -8337,7 +8576,10 @@ const SIDEBAR_TREE_JS: &str = r##"
if (files.length) {
dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
} else {
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, { rowIds: rowIds, copy: event.altKey === true || event.ctrlKey === true || event.metaKey === true }));
void Promise.resolve(ensureFileTreeWritableTarget('drop', targetRow, rowIds, event.altKey === true || event.ctrlKey === true || event.metaKey === true)).then(function(writable) {
if (!writable) return;
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, { rowIds: rowIds, copy: event.altKey === true || event.ctrlKey === true || event.metaKey === true }));
});
}
draggingFileTreeRowIds = [];
}
@@ -9195,6 +9437,17 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("确认删除选中的 "));
}
#[test]
fn sidebar_filetree_blocks_readonly_paste_and_drop_with_action_status() {
assert!(SIDEBAR_TREE_JS.contains("function blockReadonlyFileTreeAction"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-filetree-readonly-blocked"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-filetree-readonly-message"));
assert!(SIDEBAR_TREE_JS.contains("recordFileTreeActionStatus('blocked'"));
assert!(SIDEBAR_TREE_JS.contains("/api/tree/filetree/drop-preflight"));
assert!(SIDEBAR_TREE_JS.contains("ensureFileTreeWritableTarget('paste'"));
assert!(SIDEBAR_TREE_JS.contains("ensureFileTreeWritableTarget('drop'"));
}
#[test]
fn sidebar_filetree_local_table_bulk_delete_uses_tree_command() {
let table_loop_start = SIDEBAR_TREE_JS