From bc3b199c3221963458c703f5e8919e693ba2b3e1 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Tue, 26 May 2026 02:27:08 +0800 Subject: [PATCH] refactor: split sidebar filetree upload runtime --- ...ime-module-maintainability-checklist-v1.md | 2 +- .../sidebar-filetree-upload-runtime.js | 288 ++++++++++++++++++ .../mnote-web/browser/sidebar-tree-runtime.js | 283 ++--------------- rust/crates/mnote-web/src/routes/mod.rs | 5 + rust/crates/mnote-web/src/routes/web_shell.rs | 14 + rust/crates/mnote-web/src/ssr/pages/layout.rs | 26 +- 6 files changed, 348 insertions(+), 270 deletions(-) create mode 100644 rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md index 452b7730..780c2b90 100644 --- a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md +++ b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md @@ -167,7 +167,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell - [ ] C2. `sidebar-page-tree-runtime.js`:page tree row selection、rename title patch、breadcrumb/title sync。 - [x] C3. `sidebar-filetree-open-runtime.js`:local markdown / resource open target、active resource tab dispatch。 - [x] C4. `sidebar-filetree-command-runtime.js`:create/rename/delete/copy/move/trash/restore/purge command payload。 -- [ ] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。 +- [x] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。 - [x] C6. `sidebar-attachment-open-runtime.js`:OnlyOffice/PDF/code/image open mode guard。 - [x] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。 - [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。 diff --git a/rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js b/rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js new file mode 100644 index 00000000..2a7cd583 --- /dev/null +++ b/rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js @@ -0,0 +1,288 @@ +export const createSidebarFileTreeUploadRuntime = (dependencies = {}) => { + const { + cssEscape, + currentDocumentId, + fileTreeRuntimeDeps, + fileTreeRuntimeFunction, + recordFileTreeAction, + recordFileTreeActionStatus, + resolveWorkspaceId, + rowTitle, + selectedSidebarFileTreeSelection, + } = dependencies; + const sidebarFileTreeSelection = selectedSidebarFileTreeSelection; + + function fileTreeRowsForUploadPreflight() { + var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsForUploadPreflight'); + if (runtimeFn) return runtimeFn(fileTreeRuntimeDeps()); + return Array.from(document.querySelectorAll('.tree-row[data-shell-mode="filetree"]')).map(function(row) { + return { + rowId: row.getAttribute('data-row-id') || '', + 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, + assetDocumentId: row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || null, + assetType: row.querySelector('.tree-kind-badge') ? row.querySelector('.tree-kind-badge').getAttribute('data-kind') : null, + storagePath: null + }; + }).filter(function(row) { + return row.rowId || row.documentId || row.assetId; + }); + } + + 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() { + var runtimeFn = fileTreeRuntimeFunction('fileTreeDocumentParentsForPreflight'); + if (runtimeFn) return runtimeFn(fileTreeRuntimeDeps()); + 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 runtimeFn = fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight'); + if (runtimeFn) return runtimeFn(targetRow, fileTreeRuntimeDeps()); + 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() { + var runtimeFn = fileTreeRuntimeFunction('fileTreeDropPreflightRows'); + if (runtimeFn) return runtimeFn(Object.assign({}, fileTreeRuntimeDeps(), { + fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight + })); + 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 runtimeFn = fileTreeRuntimeFunction('fileTreeDocumentWorkspacesForUploadPreflight'); + if (runtimeFn) { + return runtimeFn(workspaceId, Object.assign({}, fileTreeRuntimeDeps(), { + fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight + })); + } + var seen = new Set(); + return fileTreeRowsForUploadPreflight().filter(function(row) { + if (!row.documentId || seen.has(row.documentId)) return false; + seen.add(row.documentId); + return true; + }).map(function(row) { + return { documentId: row.documentId, workspaceId: workspaceId || null }; + }); + } + + async function preflightFileTreeUploadTarget(detail) { + var bodyRuntime = fileTreeRuntimeFunction('fileTreeUploadTargetPreflightBody'); + var body = bodyRuntime ? bodyRuntime(detail || {}, Object.assign({}, fileTreeRuntimeDeps(), { + focusedRowId: sidebarFileTreeSelection.focusedRowId || null, + fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight, + currentDocumentId: currentDocumentId, + resolveWorkspaceId: resolveWorkspaceId + })) : (function() { + var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim(); + return { + workspaceId: workspaceId || null, + targetDocumentId: detail && detail.documentId ? String(detail.documentId) : null, + targetRowId: detail && detail.targetRowId ? String(detail.targetRowId) : null, + focusedRowId: sidebarFileTreeSelection.focusedRowId || null, + activeDocumentId: currentDocumentId() || null, + rows: fileTreeRowsForUploadPreflight(), + documentWorkspaces: fileTreeDocumentWorkspacesForUploadPreflight(workspaceId) + }; + })(); + var response = await fetch('/api/tree/filetree/upload-target-preflight', { + method: 'POST', + credentials: 'include', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok || !payload || !payload.plan) { + throw new Error(payload && payload.error ? payload.error : '文件树上传目标预检失败'); + } + return payload.plan; + } + + function fallbackFileTreeUploadTarget(detail) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeUploadTargetFallback'); + if (runtimeFn) { + return runtimeFn(detail || {}, { + resolveWorkspaceId: resolveWorkspaceId, + currentDocumentId: currentDocumentId + }); + } + var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim(); + var documentId = String(detail && detail.documentId || currentDocumentId() || '').trim(); + if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { + return { + workspaceId: workspaceId, + targetDocumentId: documentId, + targetMindmapId: null, + targetSubPath: null, + targetRelativePath: String(detail.targetRelativePath || ''), + uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop') + }; + } + if (!workspaceId || !documentId) { + throw new Error('请选择一个目标页面后再拖入文件'); + } + return { + workspaceId: workspaceId, + targetDocumentId: documentId, + targetMindmapId: null, + targetSubPath: null, + uploadIntent: String(detail && detail.uploadIntent || 'editor.markdown.attach') + }; + } + + async function resolveFileTreeUploadTarget(detail) { + if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { + return fallbackFileTreeUploadTarget(detail || {}); + } + try { + var plan = await preflightFileTreeUploadTarget(detail || {}); + var normalizePlanRuntime = fileTreeRuntimeFunction('normalizeFileTreeUploadTargetPlan'); + if (normalizePlanRuntime) { + return normalizePlanRuntime(detail || {}, plan); + } + if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { + plan.targetRelativePath = String(detail.targetRelativePath || ''); + } + if (detail && detail.uploadIntent) { + plan.uploadIntent = String(detail.uploadIntent); + } else if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { + plan.uploadIntent = 'filetree.folder.drop'; + } else if (!plan.uploadIntent) { + plan.uploadIntent = 'editor.markdown.attach'; + } + return plan; + } catch (error) { + console.warn('[mnote upload] upload target preflight fallback', error); + return fallbackFileTreeUploadTarget(detail || {}); + } + } + + + return { + fileTreeRowsForUploadPreflight, + fileTreeRowCapabilities, + fileTreeRowIsReadonly, + blockReadonlyFileTreeAction, + fileTreeDocumentParentsForPreflight, + fileTreeTargetChildrenForPreflight, + fileTreeDropPreflightRows, + ensureFileTreeWritableTarget, + fileTreeDocumentWorkspacesForUploadPreflight, + preflightFileTreeUploadTarget, + fallbackFileTreeUploadTarget, + resolveFileTreeUploadTarget, + }; +}; diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index c310a428..6a213b4f 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -3,6 +3,7 @@ import { createSidebarTreeLiveApplyRuntime } from './sidebar-tree-live-apply-run import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtime.js'; import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-runtime.js'; import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command-runtime.js'; +import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-runtime.js'; (function(){ if (window.__mnoteSidebarTreeRuntimeStarted) return; @@ -879,263 +880,19 @@ import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command- const fetchCurrentOnlyOfficeUserId = (...args) => sidebarFileTreeOpen.fetchCurrentOnlyOfficeUserId(...args); const openConvexAssetFromFileTree = (...args) => sidebarFileTreeOpen.openConvexAssetFromFileTree(...args); - function fileTreeRowsForUploadPreflight() { - var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsForUploadPreflight'); - if (runtimeFn) return runtimeFn(fileTreeRuntimeDeps()); - return Array.from(document.querySelectorAll('.tree-row[data-shell-mode="filetree"]')).map(function(row) { - return { - rowId: row.getAttribute('data-row-id') || '', - 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, - assetDocumentId: row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || null, - assetType: row.querySelector('.tree-kind-badge') ? row.querySelector('.tree-kind-badge').getAttribute('data-kind') : null, - storagePath: null - }; - }).filter(function(row) { - return row.rowId || row.documentId || row.assetId; - }); - } - - 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() { - var runtimeFn = fileTreeRuntimeFunction('fileTreeDocumentParentsForPreflight'); - if (runtimeFn) return runtimeFn(fileTreeRuntimeDeps()); - 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 runtimeFn = fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight'); - if (runtimeFn) return runtimeFn(targetRow, fileTreeRuntimeDeps()); - 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() { - var runtimeFn = fileTreeRuntimeFunction('fileTreeDropPreflightRows'); - if (runtimeFn) return runtimeFn(Object.assign({}, fileTreeRuntimeDeps(), { - fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight - })); - 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 runtimeFn = fileTreeRuntimeFunction('fileTreeDocumentWorkspacesForUploadPreflight'); - if (runtimeFn) { - return runtimeFn(workspaceId, Object.assign({}, fileTreeRuntimeDeps(), { - fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight - })); - } - var seen = new Set(); - return fileTreeRowsForUploadPreflight().filter(function(row) { - if (!row.documentId || seen.has(row.documentId)) return false; - seen.add(row.documentId); - return true; - }).map(function(row) { - return { documentId: row.documentId, workspaceId: workspaceId || null }; - }); - } - - async function preflightFileTreeUploadTarget(detail) { - var bodyRuntime = fileTreeRuntimeFunction('fileTreeUploadTargetPreflightBody'); - var body = bodyRuntime ? bodyRuntime(detail || {}, Object.assign({}, fileTreeRuntimeDeps(), { - focusedRowId: sidebarFileTreeSelection.focusedRowId || null, - fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight, - currentDocumentId: currentDocumentId, - resolveWorkspaceId: resolveWorkspaceId - })) : (function() { - var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim(); - return { - workspaceId: workspaceId || null, - targetDocumentId: detail && detail.documentId ? String(detail.documentId) : null, - targetRowId: detail && detail.targetRowId ? String(detail.targetRowId) : null, - focusedRowId: sidebarFileTreeSelection.focusedRowId || null, - activeDocumentId: currentDocumentId() || null, - rows: fileTreeRowsForUploadPreflight(), - documentWorkspaces: fileTreeDocumentWorkspacesForUploadPreflight(workspaceId) - }; - })(); - var response = await fetch('/api/tree/filetree/upload-target-preflight', { - method: 'POST', - credentials: 'include', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(body) - }); - var payload = await response.json().catch(function() { return null; }); - if (!response.ok || !payload || !payload.plan) { - throw new Error(payload && payload.error ? payload.error : '文件树上传目标预检失败'); - } - return payload.plan; - } - - function fallbackFileTreeUploadTarget(detail) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeUploadTargetFallback'); - if (runtimeFn) { - return runtimeFn(detail || {}, { - resolveWorkspaceId: resolveWorkspaceId, - currentDocumentId: currentDocumentId - }); - } - var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim(); - var documentId = String(detail && detail.documentId || currentDocumentId() || '').trim(); - if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { - return { - workspaceId: workspaceId, - targetDocumentId: documentId, - targetMindmapId: null, - targetSubPath: null, - targetRelativePath: String(detail.targetRelativePath || ''), - uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop') - }; - } - if (!workspaceId || !documentId) { - throw new Error('请选择一个目标页面后再拖入文件'); - } - return { - workspaceId: workspaceId, - targetDocumentId: documentId, - targetMindmapId: null, - targetSubPath: null, - uploadIntent: String(detail && detail.uploadIntent || 'editor.markdown.attach') - }; - } - - async function resolveFileTreeUploadTarget(detail) { - if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { - return fallbackFileTreeUploadTarget(detail || {}); - } - try { - var plan = await preflightFileTreeUploadTarget(detail || {}); - var normalizePlanRuntime = fileTreeRuntimeFunction('normalizeFileTreeUploadTargetPlan'); - if (normalizePlanRuntime) { - return normalizePlanRuntime(detail || {}, plan); - } - if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { - plan.targetRelativePath = String(detail.targetRelativePath || ''); - } - if (detail && detail.uploadIntent) { - plan.uploadIntent = String(detail.uploadIntent); - } else if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { - plan.uploadIntent = 'filetree.folder.drop'; - } else if (!plan.uploadIntent) { - plan.uploadIntent = 'editor.markdown.attach'; - } - return plan; - } catch (error) { - console.warn('[mnote upload] upload target preflight fallback', error); - return fallbackFileTreeUploadTarget(detail || {}); - } - } + var sidebarFileTreeUpload = null; + const fileTreeRowsForUploadPreflight = (...args) => sidebarFileTreeUpload.fileTreeRowsForUploadPreflight(...args); + const fileTreeRowCapabilities = (...args) => sidebarFileTreeUpload.fileTreeRowCapabilities(...args); + const fileTreeRowIsReadonly = (...args) => sidebarFileTreeUpload.fileTreeRowIsReadonly(...args); + const blockReadonlyFileTreeAction = (...args) => sidebarFileTreeUpload.blockReadonlyFileTreeAction(...args); + const fileTreeDocumentParentsForPreflight = (...args) => sidebarFileTreeUpload.fileTreeDocumentParentsForPreflight(...args); + const fileTreeTargetChildrenForPreflight = (...args) => sidebarFileTreeUpload.fileTreeTargetChildrenForPreflight(...args); + const fileTreeDropPreflightRows = (...args) => sidebarFileTreeUpload.fileTreeDropPreflightRows(...args); + const ensureFileTreeWritableTarget = (...args) => sidebarFileTreeUpload.ensureFileTreeWritableTarget(...args); + const fileTreeDocumentWorkspacesForUploadPreflight = (...args) => sidebarFileTreeUpload.fileTreeDocumentWorkspacesForUploadPreflight(...args); + const preflightFileTreeUploadTarget = (...args) => sidebarFileTreeUpload.preflightFileTreeUploadTarget(...args); + const fallbackFileTreeUploadTarget = (...args) => sidebarFileTreeUpload.fallbackFileTreeUploadTarget(...args); + const resolveFileTreeUploadTarget = (...args) => sidebarFileTreeUpload.resolveFileTreeUploadTarget(...args); function localUploadRuntimeFunction(name) { var runtime = window.__mnoteLocalUploadRuntime; @@ -2323,6 +2080,18 @@ import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command- const pasteSidebarFileTreeClipboard = (...args) => sidebarFileTreeCommand.pasteSidebarFileTreeClipboard(...args); const deleteSelectedSidebarFileTreeRows = (...args) => sidebarFileTreeCommand.deleteSelectedSidebarFileTreeRows(...args); + sidebarFileTreeUpload = createSidebarFileTreeUploadRuntime({ + cssEscape, + currentDocumentId, + fileTreeRuntimeDeps, + fileTreeRuntimeFunction, + recordFileTreeAction, + recordFileTreeActionStatus, + resolveWorkspaceId, + rowTitle, + selectedSidebarFileTreeSelection: sidebarFileTreeSelection, + }); + function ensureSearchModal() { var existing = document.querySelector('[data-testid="wolai-search-modal"]'); if (existing instanceof HTMLElement) return existing; diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index d2188d42..4bf646a5 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -142,6 +142,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/sidebar-filetree-command-runtime.js", get(web_shell::sidebar_filetree_command_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/sidebar-filetree-upload-runtime.js", + get(web_shell::sidebar_filetree_upload_runtime_asset), + ) .route( "/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js", get(web_shell::sidebar_attachment_open_runtime_asset), @@ -603,6 +607,7 @@ mod tests { "/api/mnote-browser-runtime/sidebar-tree-live-apply-runtime.js", "/api/mnote-browser-runtime/sidebar-filetree-open-runtime.js", "/api/mnote-browser-runtime/sidebar-filetree-command-runtime.js", + "/api/mnote-browser-runtime/sidebar-filetree-upload-runtime.js", "/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js", "/api/mnote-browser-runtime/sidebar-tree-runtime.js", "/api/mnote-browser-runtime/tree-live-controller.js", diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 8d15ce08..c2679dbb 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -798,6 +798,20 @@ pub async fn sidebar_filetree_command_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn sidebar_filetree_upload_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/sidebar-filetree-upload-runtime.js"); + Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + ) + .header(header::CACHE_CONTROL, "no-store") + .header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .body(Body::from(JS)) + .unwrap_or_else(|_| Response::new(Body::empty())) +} + pub async fn sidebar_attachment_open_runtime_asset() -> Response { const JS: &str = include_str!("../../browser/sidebar-attachment-open-runtime.js"); Response::builder() diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 9bfb0ee3..68957ae4 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -205,6 +205,8 @@ mod tests { include_str!("../../../browser/sidebar-filetree-open-runtime.js"); const SIDEBAR_FILETREE_COMMAND_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-filetree-command-runtime.js"); + const SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS: &str = + include_str!("../../../browser/sidebar-filetree-upload-runtime.js"); const SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-attachment-open-runtime.js"); const SIDEBAR_SHELL_RUNTIME_JS: &str = @@ -599,15 +601,15 @@ mod tests { assert!( SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('appendUploadedAssetRow')") ); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeUploadTargetFallback')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeRowsForUploadPreflight')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeDocumentParentsForPreflight')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('readProjection')")); @@ -741,11 +743,11 @@ mod tests { assert!(FILETREE_RUNTIME_JS.contains("requestMethod: requestMethod")); assert!(FILETREE_RUNTIME_JS .contains("requestBodyHasMindmapCreateOnly: requestBodyHasMindmapCreateOnly")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeDocumentWorkspacesForUploadPreflight')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeUploadTargetPreflightBody')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('shortMindmapFileName')")); assert!( @@ -1023,13 +1025,13 @@ mod tests { #[test] fn sidebar_filetree_blocks_readonly_paste_and_drop_with_action_status() { - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function blockReadonlyFileTreeAction")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-readonly-blocked")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-readonly-message")); + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("function blockReadonlyFileTreeAction")); + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("data-mnote-filetree-readonly-blocked")); + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("data-mnote-filetree-readonly-message")); assert!( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("recordFileTreeActionStatus('blocked'") ); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/filetree/drop-preflight")); + assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("/api/tree/filetree/drop-preflight")); assert!( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("ensureFileTreeWritableTarget('paste'") );