From 19642cdd7b79fe3956f01d417bcc6f260c04fcad Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Tue, 26 May 2026 02:53:12 +0800 Subject: [PATCH] refactor: split tree shell filetree helpers --- ...ime-module-maintainability-checklist-v1.md | 2 +- .../browser/tree-shell-filetree-runtime.js | 164 +++++++++++++++ .../mnote-web/browser/tree-shell-runtime.js | 186 ++---------------- rust/crates/mnote-web/src/routes/mod.rs | 5 + rust/crates/mnote-web/src/routes/tree.rs | 7 +- rust/crates/mnote-web/src/routes/web_shell.rs | 14 ++ 6 files changed, 206 insertions(+), 172 deletions(-) create mode 100644 rust/crates/mnote-web/browser/tree-shell-filetree-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 0f26b341..cc1fabfd 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 @@ -206,7 +206,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib ssr::pages::layout - [x] D1. `tree-shell-state-runtime.js`:state hydration、serialization、patch dispatch。 - [x] D2. `tree-shell-page-runtime.js`:page tree keyboard、expand、focus、drag/drop intent。 -- [ ] D3. `tree-shell-filetree-runtime.js`:filetree row normalization、resource meta、open target。 +- [x] D3. `tree-shell-filetree-runtime.js`:filetree row normalization、resource meta、open target。 - [ ] D4. `tree-shell-picker-runtime.js`:picker search/focus/pick root。 - [ ] D5. `tree-shell-dom-runtime.js`:DOM patch/render helpers。 - [x] D6. `tree-shell-icons-runtime.js`:icon templates 和 resource kind badge。 diff --git a/rust/crates/mnote-web/browser/tree-shell-filetree-runtime.js b/rust/crates/mnote-web/browser/tree-shell-filetree-runtime.js new file mode 100644 index 00000000..016432da --- /dev/null +++ b/rust/crates/mnote-web/browser/tree-shell-filetree-runtime.js @@ -0,0 +1,164 @@ +// MNote debug /tree shell filetree row normalization helpers. + +import { + normalizeTreeShellNumber, + normalizeTreeShellText, +} from "./tree-shell-state-runtime.js"; + +export function buildFileTreeRuntimeEnvironment(runtimeContext) { + const fileTreeRowById = runtimeContext.fileTreeRowById || new Map(); + return { + visibleRowIds: Array.isArray(runtimeContext.visibleFileTreeRowIds) + ? runtimeContext.visibleFileTreeRowIds + : [], + rows: Array.from(fileTreeRowById.values()).map((entry) => ({ + rowId: entry.rowId, + rowKind: entry.rowKind, + documentId: entry.documentId || null, + assetId: entry.assetId || null, + })), + rootUri: runtimeContext.sourceKind === "local_folder" ? runtimeContext.rootUri || null : null, + }; +} + +export function compareTreeShellItems(left, right) { + const byPosition = left.position - right.position; + if (byPosition !== 0) return byPosition; + return left.title.localeCompare(right.title, "zh-CN"); +} + +export function normalizeTreeShellResourceMeta(value) { + if (!value || typeof value !== "object") { + return { + resourceKind: "", + documentId: "", + assetId: "", + assetKind: "", + objectIdentity: null, + blockAssetRelation: null, + }; + } + const objectIdentity = + value?.objectIdentity && typeof value.objectIdentity === "object" + ? value.objectIdentity + : null; + const blockAssetRelation = + value?.blockAssetRelation && typeof value.blockAssetRelation === "object" + ? value.blockAssetRelation + : null; + return { + resourceKind: normalizeTreeShellText(value?.resourceKind), + documentId: normalizeTreeShellText(value?.documentId), + assetId: normalizeTreeShellText(value?.assetId), + assetKind: normalizeTreeShellText(value?.assetKind), + objectIdentity, + blockAssetRelation, + }; +} + +export function normalizeTreeShellRowKind(value) { + const normalized = normalizeTreeShellText(value).toLowerCase(); + if (normalized === "index") return "index"; + if (normalized === "asset") return "asset"; + if (normalized === "folder") return "folder"; + if (normalized === "markdown") return "markdown"; + if (normalized === "asset_folder") return "asset_folder"; + return "document"; +} + +export function normalizeTreeShellTreeItems(items, options = {}) { + const excludedIds = options.excludedIds instanceof Set ? options.excludedIds : new Set(); + return Array.isArray(items) + ? items + .map((item) => { + const nodeId = normalizeTreeShellText(item?.nodeId); + const resourceMeta = normalizeTreeShellResourceMeta(item?.resourceMeta); + const rowKind = normalizeTreeShellRowKind(item?.rowKind); + const fallbackRowId = + rowKind === "index" + ? `index:${resourceMeta.documentId || nodeId.replace(/^index:/, "")}` + : rowKind === "asset" + ? `asset:${resourceMeta.assetId || nodeId.replace(/^asset:/, "")}` + : rowKind === "asset_folder" + ? `asset-folder:${resourceMeta.assetId || nodeId.replace(/^asset-folder:/, "")}` + : `doc:${resourceMeta.documentId || nodeId}`; + return { + rowId: normalizeTreeShellText(item?.rowId, fallbackRowId), + rowKind, + nodeId, + parentNodeId: normalizeTreeShellText(item?.parentNodeId) || null, + title: normalizeTreeShellText(item?.title, rowKind === "index" ? "index.md" : "无标题"), + depth: normalizeTreeShellNumber(item?.depth, 0), + childCount: normalizeTreeShellNumber(item?.childCount, 0), + position: normalizeTreeShellNumber(item?.position), + expandedByDefault: item?.expandedByDefault !== false, + iconHint: normalizeTreeShellText(item?.iconHint), + capabilities: Array.isArray(item?.capabilities) + ? item.capabilities + .map((capability) => normalizeTreeShellText(capability)) + .filter(Boolean) + : [], + resourceMeta, + }; + }) + .filter((item) => item.nodeId && !excludedIds.has(item.nodeId)) + : []; +} + +export function getFileTreeRowDocumentId(item) { + if (!item) return ""; + if (item.rowKind === "document" || item.rowKind === "markdown") return item.nodeId; + if (item.rowKind === "index") return item.nodeId.replace(/^index:/, ""); + return ""; +} + +export function getFileTreeRowOwnerDocumentId(item) { + if (!item) return ""; + if (item.resourceMeta?.documentId) return item.resourceMeta.documentId; + return getFileTreeRowDocumentId(item); +} + +export function getFileTreeRowAssetId(item) { + if (!item) return ""; + if (item.resourceMeta?.assetId) return item.resourceMeta.assetId; + if (item.rowKind === "asset") return item.nodeId.replace(/^asset:/, ""); + if (item.rowKind === "asset_folder") return item.nodeId.replace(/^asset-folder:/, ""); + return ""; +} + +export function getFileTreeRowIconKind(item) { + if (!item) return "file"; + const iconHint = normalizeTreeShellText(item.iconHint).toLowerCase(); + if (iconHint === "mindmap") return "mindmap"; + if (iconHint === "table") return "table"; + if (iconHint === "index") return "index"; + if (iconHint === "page") return "page"; + if (item.rowKind === "document") return "page"; + if (item.rowKind === "folder") return "folder"; + if (item.rowKind === "markdown") return "file"; + if (item.rowKind === "index") return "index"; + if (item.rowKind === "asset_folder") return "mindmap"; + if (item.resourceMeta?.resourceKind === "table") return "table"; + if (item.resourceMeta?.resourceKind === "mindmap") return "mindmap"; + return "file"; +} + +export function getFileTreeRowMetaLabel(item) { + if (!item) return ""; + if (item.rowKind === "document") { + return `${getFileTreeRowDocumentId(item)} · 页面`; + } + if (item.rowKind === "folder") { + return "本地文件夹"; + } + if (item.rowKind === "markdown") { + return `${getFileTreeRowDocumentId(item)} · Markdown`; + } + if (item.rowKind === "index") { + return "页面正文"; + } + if (item.rowKind === "asset_folder") { + return `${item.resourceMeta?.resourceKind || "mindmap"} · 资源目录`; + } + return `${item.resourceMeta?.resourceKind || item.resourceMeta?.assetKind || "asset"} · ${getFileTreeRowAssetId(item)}`; +} diff --git a/rust/crates/mnote-web/browser/tree-shell-runtime.js b/rust/crates/mnote-web/browser/tree-shell-runtime.js index 2aaf5756..0a5c9e9b 100644 --- a/rust/crates/mnote-web/browser/tree-shell-runtime.js +++ b/rust/crates/mnote-web/browser/tree-shell-runtime.js @@ -10,6 +10,16 @@ import { createTreeShellActionButton, createTreeShellKindBadge as createKindBadge, } from "./tree-shell-icons-runtime.js"; +import { + buildFileTreeRuntimeEnvironment, + compareTreeShellItems, + getFileTreeRowAssetId, + getFileTreeRowDocumentId, + getFileTreeRowIconKind, + getFileTreeRowMetaLabel, + getFileTreeRowOwnerDocumentId, + normalizeTreeShellTreeItems, +} from "./tree-shell-filetree-runtime.js"; import { normalizeTreeShellNumber as normalizeNumber, normalizeTreeShellStringArray as normalizeStringArray, @@ -20,22 +30,6 @@ import { resolveTreeShellTargetOrigin, } from "./tree-shell-state-runtime.js"; -function buildFileTreeRuntimeEnvironment(runtimeContext) { - const fileTreeRowById = runtimeContext.fileTreeRowById || new Map(); - return { - visibleRowIds: Array.isArray(runtimeContext.visibleFileTreeRowIds) - ? runtimeContext.visibleFileTreeRowIds - : [], - rows: Array.from(fileTreeRowById.values()).map((entry) => ({ - rowId: entry.rowId, - rowKind: entry.rowKind, - documentId: entry.documentId || null, - assetId: entry.assetId || null, - })), - rootUri: runtimeContext.sourceKind === "local_folder" ? runtimeContext.rootUri || null : null, - }; -} - function startTreeShellRuntime() { const stateElement = document.getElementById("tree-shell-state"); const appElement = document.getElementById("tree-shell-app"); @@ -173,98 +167,8 @@ function startTreeShellRuntime() { "/api/tree/runtime/reduce", ); - const normalizeParent = (value) => { - const normalized = normalizeText(value); - return normalized || null; - }; - - const normalizeRowKind = (value) => { - const normalized = normalizeText(value).toLowerCase(); - if (normalized === "index") return "index"; - if (normalized === "asset") return "asset"; - if (normalized === "folder") return "folder"; - if (normalized === "markdown") return "markdown"; - if (normalized === "asset_folder") return "asset_folder"; - return "document"; - }; - - const normalizeCapabilities = (value) => - Array.isArray(value) - ? value - .map((item) => normalizeText(item)) - .filter(Boolean) - : []; - - const normalizeResourceMeta = (value) => { - if (!value || typeof value !== "object") { - return { - resourceKind: "", - documentId: "", - assetId: "", - assetKind: "", - objectIdentity: null, - blockAssetRelation: null, - }; - } - const objectIdentity = - value?.objectIdentity && typeof value.objectIdentity === "object" - ? value.objectIdentity - : null; - const blockAssetRelation = - value?.blockAssetRelation && typeof value.blockAssetRelation === "object" - ? value.blockAssetRelation - : null; - return { - resourceKind: normalizeText(value?.resourceKind), - documentId: normalizeText(value?.documentId), - assetId: normalizeText(value?.assetId), - assetKind: normalizeText(value?.assetKind), - objectIdentity, - blockAssetRelation, - }; - }; - - const compareItems = (left, right) => { - const byPosition = left.position - right.position; - if (byPosition !== 0) return byPosition; - return left.title.localeCompare(right.title, "zh-CN"); - }; - - const normalizeTreeItems = (items) => - Array.isArray(items) - ? items - .map((item) => { - const nodeId = normalizeText(item?.nodeId); - const resourceMeta = normalizeResourceMeta(item?.resourceMeta); - const rowKind = normalizeRowKind(item?.rowKind); - const fallbackRowId = - rowKind === "index" - ? `index:${resourceMeta.documentId || nodeId.replace(/^index:/, "")}` - : rowKind === "asset" - ? `asset:${resourceMeta.assetId || nodeId.replace(/^asset:/, "")}` - : rowKind === "asset_folder" - ? `asset-folder:${resourceMeta.assetId || nodeId.replace(/^asset-folder:/, "")}` - : `doc:${resourceMeta.documentId || nodeId}`; - return { - rowId: normalizeText(item?.rowId, fallbackRowId), - rowKind, - nodeId, - parentNodeId: normalizeParent(item?.parentNodeId), - title: normalizeText(item?.title, rowKind === "index" ? "index.md" : "无标题"), - depth: normalizeNumber(item?.depth, 0), - childCount: normalizeNumber(item?.childCount, 0), - position: normalizeNumber(item?.position), - expandedByDefault: item?.expandedByDefault !== false, - iconHint: normalizeText(item?.iconHint), - capabilities: normalizeCapabilities(item?.capabilities), - resourceMeta, - }; - }) - .filter((item) => item.nodeId && !excludedIds.has(item.nodeId)) - : []; - let rawItems = Array.isArray(hostOverride.items) ? hostOverride.items : state.items; - let normalizedItems = normalizeTreeItems(rawItems); + let normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds }); let itemById = new Map(); let fileTreeRowById = new Map(); let childrenByParentId = new Map(); @@ -301,8 +205,8 @@ function startTreeShellRuntime() { bucket.push(item); childrenByParentId.set(parentId, bucket); }); - roots.sort(compareItems); - childrenByParentId.forEach((bucket) => bucket.sort(compareItems)); + roots.sort(compareTreeShellItems); + childrenByParentId.forEach((bucket) => bucket.sort(compareTreeShellItems)); }; rebuildTreeIndexes(); @@ -448,64 +352,6 @@ function startTreeShellRuntime() { const FILETREE_DRAG_MIME = "application/x-mnote-filetree-row-ids"; - const getFileTreeRowDocumentId = (item) => { - if (!item) return ""; - if (item.rowKind === "document" || item.rowKind === "markdown") return item.nodeId; - if (item.rowKind === "index") return item.nodeId.replace(/^index:/, ""); - return ""; - }; - - const getFileTreeRowOwnerDocumentId = (item) => { - if (!item) return ""; - if (item.resourceMeta?.documentId) return item.resourceMeta.documentId; - return getFileTreeRowDocumentId(item); - }; - - const getFileTreeRowAssetId = (item) => { - if (!item) return ""; - if (item.resourceMeta?.assetId) return item.resourceMeta.assetId; - if (item.rowKind === "asset") return item.nodeId.replace(/^asset:/, ""); - if (item.rowKind === "asset_folder") return item.nodeId.replace(/^asset-folder:/, ""); - return ""; - }; - - const getFileTreeRowIconKind = (item) => { - if (!item) return "file"; - const iconHint = normalizeText(item.iconHint).toLowerCase(); - if (iconHint === "mindmap") return "mindmap"; - if (iconHint === "table") return "table"; - if (iconHint === "index") return "index"; - if (iconHint === "page") return "page"; - if (item.rowKind === "document") return "page"; - if (item.rowKind === "folder") return "folder"; - if (item.rowKind === "markdown") return "file"; - if (item.rowKind === "index") return "index"; - if (item.rowKind === "asset_folder") return "mindmap"; - if (item.resourceMeta?.resourceKind === "table") return "table"; - if (item.resourceMeta?.resourceKind === "mindmap") return "mindmap"; - return "file"; - }; - - const getFileTreeRowMetaLabel = (item) => { - if (!item) return ""; - if (item.rowKind === "document") { - return `${getFileTreeRowDocumentId(item)} · 页面`; - } - if (item.rowKind === "folder") { - return "本地文件夹"; - } - if (item.rowKind === "markdown") { - return `${getFileTreeRowDocumentId(item)} · Markdown`; - } - if (item.rowKind === "index") { - return "页面正文"; - } - if (item.rowKind === "asset_folder") { - return `${item.resourceMeta?.resourceKind || "mindmap"} · 资源目录`; - } - return `${item.resourceMeta?.resourceKind || item.resourceMeta?.assetKind || "asset"} · ${getFileTreeRowAssetId(item)}`; - }; - const canExpandFileTreeRow = (item) => { if (!item) return false; return item.childCount > 0 || item.capabilities.includes("expand"); @@ -1522,7 +1368,7 @@ function startTreeShellRuntime() { mindmapAssets = Array.isArray(nextState.mindmapAssets) ? nextState.mindmapAssets : []; tableAssets = Array.isArray(nextState.tableAssets) ? nextState.tableAssets : []; rawItems = Array.isArray(nextState.items) ? nextState.items : []; - normalizedItems = normalizeTreeItems(rawItems); + normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds }); rebuildTreeIndexes(); if (currentActiveDocumentId && !itemById.has(currentActiveDocumentId)) { currentActiveDocumentId = roots[0]?.nodeId || ""; @@ -1573,14 +1419,14 @@ function startTreeShellRuntime() { if (parentId) { const bucket = childrenByParentId.get(parentId) || []; bucket.push(item); - bucket.sort(compareItems); + bucket.sort(compareTreeShellItems); childrenByParentId.set(parentId, bucket); const parentItem = itemById.get(parentId); if (parentItem) parentItem.childCount = Math.max(parentItem.childCount || 0, bucket.length); expanded.add(parentId); } else { roots.push(item); - roots.sort(compareItems); + roots.sort(compareTreeShellItems); } return true; }; diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index e6425aa8..a31d674d 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -178,6 +178,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/tree-shell-icons-runtime.js", get(web_shell::tree_shell_icons_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/tree-shell-filetree-runtime.js", + get(web_shell::tree_shell_filetree_runtime_asset), + ) .route( "/api/mnote-browser-runtime/document-conflict-panel-runtime.js", get(web_shell::document_conflict_panel_runtime_asset), @@ -632,6 +636,7 @@ mod tests { "/api/mnote-browser-runtime/tree-shell-page-runtime.js", "/api/mnote-browser-runtime/tree-shell-state-runtime.js", "/api/mnote-browser-runtime/tree-shell-icons-runtime.js", + "/api/mnote-browser-runtime/tree-shell-filetree-runtime.js", "/api/mnote-browser-runtime/document-conflict-panel-runtime.js", "/api/mnote-browser-runtime/document-pane-runtime.js", "/api/mnote-browser-runtime/document-mindmap-host-runtime.js", diff --git a/rust/crates/mnote-web/src/routes/tree.rs b/rust/crates/mnote-web/src/routes/tree.rs index 282c5dc6..35f29ded 100644 --- a/rust/crates/mnote-web/src/routes/tree.rs +++ b/rust/crates/mnote-web/src/routes/tree.rs @@ -2393,6 +2393,8 @@ pub async fn reduce_tree_shell_runtime( #[cfg(test)] mod tests { const TREE_SHELL_RUNTIME_JS: &str = include_str!("../../browser/tree-shell-runtime.js"); + const TREE_SHELL_FILETREE_RUNTIME_JS: &str = + include_str!("../../browser/tree-shell-filetree-runtime.js"); use super::{ collect_filetree_render_rows, create_command_wire, TreeCommandEnvelopeContext, @@ -2673,7 +2675,10 @@ mod tests { assert!(TREE_SHELL_RUNTIME_JS.contains("tree.filetree.external-drop")); assert!(html.contains("\"rowKind\":\"asset_folder\"")); assert!(html.contains("\"resourceMeta\"")); - assert!(TREE_SHELL_RUNTIME_JS.contains("const getFileTreeRowOwnerDocumentId = (item) => {")); + assert!(TREE_SHELL_RUNTIME_JS.contains("tree-shell-filetree-runtime.js")); + assert!( + TREE_SHELL_FILETREE_RUNTIME_JS.contains("function getFileTreeRowOwnerDocumentId(item)") + ); assert!(TREE_SHELL_RUNTIME_JS .contains("row.dataset.ownerDocumentId = ownerDocumentId || \"\";")); assert!( diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index cf01577e..0d70ec46 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -980,6 +980,20 @@ pub async fn tree_shell_icons_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn tree_shell_filetree_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/tree-shell-filetree-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 document_conflict_panel_runtime_asset() -> Response { // include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/) const JS: &str = include_str!("../../browser/document-conflict-panel-runtime.js");