refactor: delegate filetree asset append
This commit is contained in:
@@ -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 仍未迁出或覆盖。
|
||||
|
||||
@@ -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, '"')
|
||||
.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 =
|
||||
'<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(objectKind || 'attachment') + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
|
||||
'<span class="tree-spacer" aria-hidden="true"></span><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('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button>' +
|
||||
'<div class="tree-actions"><button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml('asset:' + assetId) + '" aria-label="更多操作">…</button></div></div>';
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user