From 33ac2d927c19608e0fe74b92b02ae4e3cd91e138 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Mon, 25 May 2026 03:14:54 +0800 Subject: [PATCH] refactor: delegate filetree asset append --- ...0-browser-runtime-followup-checklist-v1.md | 7 ++ .../mnote-web/browser/filetree-runtime.js | 116 ++++++++++++++++++ rust/crates/mnote-web/src/ssr/pages/layout.rs | 21 +++- 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md index 72d95eff..a0a98a02 100644 --- a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md +++ b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md @@ -133,3 +133,10 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 修改:`document-conflict-panel-runtime.js` 新增并导出 `runSessionConflictAction(action, deps)`,只负责 promise/catch 动作包装;`web_shell.rs` 仍持有 `acceptDiskVersion`、`keepCurrentEditorVersion`、`writeMergedConflictResult`、status 和重新渲染业务逻辑。 - 已验证:`node --check rust/crates/mnote-web/browser/document-conflict-panel-runtime.js`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web document_conflict_panel_runtime_contains_dom_helpers -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 - 未完成:`acceptDiskVersion` / `keepCurrentEditorVersion` / `writeMergedConflictResult` 业务逻辑、document session lifecycle、secondary pane conflict smoke 仍未迁出或覆盖。 +- 2026-05-25:Batch I 只派发 1 个只读 Reasonix worker 审计 filetree DOM helper 边界,使用独立 worktree `/mnt/Data1T/mnote-worktrees/0525-i-3-20-filetree-dom-readonly`,run id `reasonix-2026-05-24T19-09-00-815Z-4a0a3df4`,并显式使用 `--no-memory-recall` 降低任务串线风险。 + - Codex 已读取 Hindsight recall、`process-handoff.md/json`、`result.json`,并核对 worktree `git status --short` / `git diff --stat` 为空;runner 完成后由主控终止残留进程。 + - 审计结论:`revealFileTreeRow`、`revealFileTreeAssetRow`、`appendUploadedAssetRow` 可迁到 `filetree-runtime.js`;`refreshLocalFolderSidebarSnapshot` 仍是 fetch / projection render / selection / focus / attachment existence 的 shell 编排,不迁。 + - Codex 主控完成 filetree DOM append 小切片:`filetree-runtime.js` 新增并导出 `revealFileTreeRow`、`revealFileTreeAssetRow`、`appendUploadedAssetRow`;`layout.rs` 的同名 wrapper 优先委托 runtime,保留完整 inline fallback,并通过 `fileTreeRuntimeDeps` 注入 `escapeHtml`、`objectIdentityAttr`、上传资源标题/类型 helper 和当前 source/document。 + - 修改:`rust/crates/mnote-web/browser/filetree-runtime.js`、`rust/crates/mnote-web/src/ssr/pages/layout.rs`。 + - 已验证:`node --check rust/crates/mnote-web/browser/filetree-runtime.js`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_runtime -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder -- --test-threads=1`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 + - 未完成:`refreshLocalFolderSidebarSnapshot`、async preflight、`acceptDiskVersion` / `keepCurrentEditorVersion` / `writeMergedConflictResult` 业务逻辑、document session lifecycle、secondary pane conflict smoke 仍未迁出或覆盖。 diff --git a/rust/crates/mnote-web/browser/filetree-runtime.js b/rust/crates/mnote-web/browser/filetree-runtime.js index dcddc252..5695d578 100644 --- a/rust/crates/mnote-web/browser/filetree-runtime.js +++ b/rust/crates/mnote-web/browser/filetree-runtime.js @@ -120,6 +120,119 @@ function fileTreeChildCount(documentId, deps) { return children.length; } +function defaultEscapeHtml(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function defaultObjectIdentityAttr(identity) { + try { + return JSON.stringify(identity || {}); + } catch (_) { + return ''; + } +} + +function revealFileTreeRow(row) { + if (!(row instanceof HTMLElement)) return; + var node = row.closest('.tree-node'); + while (node && node.parentElement) { + if (node.parentElement.classList && node.parentElement.classList.contains('tree-children')) { + node.parentElement.classList.remove('tree-children--collapsed'); + var parentNode = node.parentElement.closest('.tree-node'); + var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row') : null; + if (parentRow instanceof HTMLElement) { + parentRow.setAttribute('aria-expanded', 'true'); + var toggle = parentRow.querySelector('[data-rust-action="toggle"]'); + if (toggle) toggle.setAttribute('aria-expanded', 'true'); + } + } + node = node.parentElement.closest('.tree-node'); + } + try { row.scrollIntoView({ block: 'nearest' }); } catch (_) {} +} + +function revealFileTreeAssetRow(assetId, deps) { + if (!assetId) return false; + var cssEscape = deps && typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); }; + var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(assetId) + '"]'); + if (!(row instanceof HTMLElement)) return false; + revealFileTreeRow(row); + return true; +} + +function appendUploadedAssetRow(asset, documentId, deps) { + deps = deps || {}; + var cssEscape = typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); }; + var escapeHtml = typeof deps.escapeHtml === 'function' ? deps.escapeHtml : defaultEscapeHtml; + var objectIdentityAttr = typeof deps.objectIdentityAttr === 'function' ? deps.objectIdentityAttr : defaultObjectIdentityAttr; + var uploadedAssetTitle = typeof deps.uploadedAssetTitle === 'function' ? deps.uploadedAssetTitle : function(nextAsset) { + return String(nextAsset && (nextAsset.file_name || nextAsset.title || nextAsset.name) || '未命名附件').trim() || '未命名附件'; + }; + var uploadedAssetType = typeof deps.uploadedAssetType === 'function' ? deps.uploadedAssetType : function(nextAsset) { + return String(nextAsset && (nextAsset.asset_type || nextAsset.assetType || nextAsset.mime_type || '') || '').trim(); + }; + var fileTreeIconKindForFileName = typeof deps.fileTreeIconKindForFileName === 'function' ? deps.fileTreeIconKindForFileName : function() { return ''; }; + var currentDocumentId = typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId : function() { return ''; }; + var currentSourceKind = typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; }; + var assetId = String(asset && asset.id || '').trim(); + if (!assetId) return false; + if (revealFileTreeAssetRow(assetId, { cssEscape: cssEscape })) return true; + var objectKind = String(asset && (asset.objectKind || asset.resourceKind || '') || '').trim(); + if (!objectKind && (String(asset && (asset.asset_type || asset.assetType) || '').trim() === 'mindmap' || /\.mindmap\.json$/i.test(assetId))) { + objectKind = 'mindmap'; + } + var targetDocumentId = String(documentId || asset.document_id || asset.documentId || currentDocumentId() || '').trim(); + var parentRow = targetDocumentId + ? document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + targetDocumentId) + '"]') + : null; + if (!parentRow && currentSourceKind() === 'local_folder' && objectKind === 'mindmap') return false; + if (!parentRow) parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-kind="document"]'); + var root = document.querySelector('#sidebar-file-tree-root .tree-root'); + if (!root && !parentRow) return false; + var parentLi = parentRow ? parentRow.closest('.tree-node') : null; + var children = parentLi ? parentLi.querySelector(':scope > .tree-children') : null; + if (parentLi && !children) { + children = document.createElement('ul'); + children.className = 'tree-children'; + parentLi.appendChild(children); + } + if (children) { + children.classList.remove('tree-children--collapsed'); + if (parentRow) { + parentRow.setAttribute('aria-expanded', 'true'); + var toggle = parentRow.querySelector('[data-rust-action="toggle"]'); + if (toggle) toggle.setAttribute('aria-expanded', 'true'); + } + } + var container = children || root; + var li = document.createElement('li'); + li.className = 'tree-node'; + var objectIdentity = { + objectKind: objectKind || 'attachment', + documentId: targetDocumentId || null, + blockId: null, + assetId: assetId + }; + li.setAttribute('data-node-id', 'asset:' + assetId); + var title = uploadedAssetTitle(asset); + var iconKind = fileTreeIconKindForFileName(title) || uploadedAssetType(asset) || 'file'; + if (objectKind === 'mindmap') iconKind = 'mindmap'; + li.innerHTML = + ''; + container.appendChild(li); + revealFileTreeRow(li.querySelector('.tree-row')); + document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId); + return true; +} + function fileTreeUploadTargetFallback(detail, deps) { detail = detail || {}; deps = deps || {}; @@ -161,5 +274,8 @@ window.__mnoteFileTreeRuntime = { fileTreeAssetDownloadDetail: fileTreeAssetDownloadDetail, fileTreeRowsByRowIds: fileTreeRowsByRowIds, fileTreeChildCount: fileTreeChildCount, + revealFileTreeRow: revealFileTreeRow, + revealFileTreeAssetRow: revealFileTreeAssetRow, + appendUploadedAssetRow: appendUploadedAssetRow, fileTreeUploadTargetFallback: fileTreeUploadTargetFallback }; diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index d943aec6..56d04ec6 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -3162,10 +3162,16 @@ const SIDEBAR_TREE_JS: &str = r##" function fileTreeRuntimeDeps() { return { currentSourceKind: currentSourceKind, + currentDocumentId: currentDocumentId, localFilePathFromAssetId: localFilePathFromAssetId, rowTitle: rowTitle, resolveWorkspaceId: resolveWorkspaceId, - cssEscape: cssEscape + cssEscape: cssEscape, + escapeHtml: escapeHtml, + objectIdentityAttr: objectIdentityAttr, + uploadedAssetTitle: uploadedAssetTitle, + uploadedAssetType: uploadedAssetType, + fileTreeIconKindForFileName: fileTreeIconKindForFileName }; } @@ -3529,6 +3535,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function revealFileTreeRow(row) { + var runtimeFn = fileTreeRuntimeFunction('revealFileTreeRow'); + if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); if (!(row instanceof HTMLElement)) return; var node = row.closest('.tree-node'); while (node && node.parentElement) { @@ -3548,6 +3556,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function revealFileTreeAssetRow(assetId) { + var runtimeFn = fileTreeRuntimeFunction('revealFileTreeAssetRow'); + if (runtimeFn) return runtimeFn(assetId, fileTreeRuntimeDeps()); if (!assetId) return false; var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(assetId) + '"]'); if (!(row instanceof HTMLElement)) return false; @@ -3556,6 +3566,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function appendUploadedAssetRow(asset, documentId) { + var runtimeFn = fileTreeRuntimeFunction('appendUploadedAssetRow'); + if (runtimeFn) return runtimeFn(asset, documentId, fileTreeRuntimeDeps()); var assetId = String(asset && asset.id || '').trim(); if (!assetId) return false; if (revealFileTreeAssetRow(assetId)) return true; @@ -11038,6 +11050,9 @@ mod tests { assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeAssetDownloadDetail')")); assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeRowsByRowIds')")); assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeChildCount')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('revealFileTreeRow')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('revealFileTreeAssetRow')")); + assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('appendUploadedAssetRow')")); assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeUploadTargetFallback')")); let document_id_body = js_function_body(SIDEBAR_TREE_JS, "fileTreeRowDocumentId"); assert!( @@ -11058,7 +11073,11 @@ mod tests { assert!(FILETREE_RUNTIME_JS.contains("function fileTreeAssetDownloadDetail")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowsByRowIds")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeChildCount")); + assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeRow")); + assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeAssetRow")); + assert!(FILETREE_RUNTIME_JS.contains("function appendUploadedAssetRow")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeUploadTargetFallback")); + assert!(FILETREE_RUNTIME_JS.contains("appendUploadedAssetRow: appendUploadedAssetRow")); assert!(FILETREE_RUNTIME_JS .contains("uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop')")); assert!(FILETREE_RUNTIME_JS