refactor: split sidebar tree live apply runtime

This commit is contained in:
lix-2026
2026-05-26 01:51:07 +08:00
parent 5a18631af8
commit f2d4ec8086
6 changed files with 1060 additions and 931 deletions
+5
View File
@@ -130,6 +130,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/mnote-browser-runtime/sidebar-workspace-runtime.js",
get(web_shell::sidebar_workspace_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/sidebar-tree-live-apply-runtime.js",
get(web_shell::sidebar_tree_live_apply_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
get(web_shell::sidebar_tree_runtime_asset),
@@ -584,6 +588,7 @@ mod tests {
"/api/mnote-browser-runtime/filetree-keyboard-runtime.js",
"/api/mnote-browser-runtime/sidebar-shell-runtime.js",
"/api/mnote-browser-runtime/sidebar-workspace-runtime.js",
"/api/mnote-browser-runtime/sidebar-tree-live-apply-runtime.js",
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
"/api/mnote-browser-runtime/tree-live-controller.js",
"/api/mnote-browser-runtime/tree-shell-runtime.js",
@@ -756,6 +756,20 @@ pub async fn sidebar_workspace_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn sidebar_tree_live_apply_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/sidebar-tree-live-apply-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 filetree_keyboard_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/filetree-keyboard-runtime.js");
Response::builder()
+90 -68
View File
@@ -199,6 +199,8 @@ pub fn PageLayout(
#[cfg(test)]
mod tests {
const SIDEBAR_TREE_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-tree-runtime.js");
const SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-tree-live-apply-runtime.js");
const SIDEBAR_SHELL_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-shell-runtime.js");
const SIDEBAR_WORKSPACE_RUNTIME_JS: &str =
@@ -248,14 +250,18 @@ mod tests {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'create"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'remove"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'rename"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'move"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function sortOrderFromDelta(data)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data.sortOrder ?? data.sort_order"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function insertTreeNodeAtSortOrder"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'create"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'remove"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'rename"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'move"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function sortOrderFromDelta(data)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data.sortOrder ?? data.sort_order"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function insertTreeNodeAtSortOrder"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("wolai:assets-changed"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyAssetsChangedToFileTree"));
@@ -474,20 +480,22 @@ mod tests {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("startLocalFolderSidebarWatch"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentWorkspaceId()"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/file"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderSidebarSnapshot(sidebarPayload.result || sidebarPayload)"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileProjection(filePayload.result || filePayload)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied"));
assert!(!SIDEBAR_TREE_RUNTIME_JS
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied")
);
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fetch(window.location.href, { headers: { accept: 'text/html' } })"));
assert!(!SIDEBAR_TREE_RUNTIME_JS
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("replaceSidebarTreeFromDocument(nextDocument, 'sidebar-tree-root')"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("window.location.reload"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("window.location.reload"));
}
#[test]
@@ -589,25 +597,31 @@ mod tests {
.contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readProjection')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readSidebarDataset')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('readProjection')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('readSidebarDataset')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('readDatasetProjection')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('projectionItems')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('hasProjectionItems')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('nodeIdOf')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('rowIdOf')"));
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readDatasetProjection')")
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('parentIdOf')")
);
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('projectionItems')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('hasProjectionItems')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('nodeIdOf')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('rowIdOf')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('parentIdOf')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('groupRowsByParent')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('groupRowsByParent')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('replaceSidebarTreeFromDocument')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')"));
let document_id_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "fileTreeRowDocumentId");
assert!(
@@ -615,15 +629,19 @@ mod tests {
&& document_id_body.contains("data-doc-id"),
"inline fallback 必须保留旧 data attribute 解析"
);
let replace_body =
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "replaceSidebarTreeFromDocument");
let replace_body = js_function_body(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
"replaceSidebarTreeFromDocument",
);
assert!(
replace_body.contains("current.innerHTML = next.innerHTML")
&& replace_body.contains("current.setAttribute(attr.name, attr.value)"),
"inline fallback 必须保留 DOM copy 行为"
);
let apply_body =
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "applyLocalFolderSidebarSnapshot");
let apply_body = js_function_body(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
"applyLocalFolderSidebarSnapshot",
);
assert!(
apply_body.contains("options.pageRootId || 'sidebar-tree-root'")
&& apply_body.contains("options.fileRootId || 'sidebar-file-tree-root'")
@@ -631,8 +649,10 @@ mod tests {
&& apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, fileRootId)"),
"inline fallback 必须保留 sidebar/page tree DOM 应用行为"
);
let watch_applied_body =
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "markLocalFolderWatchApplied");
let watch_applied_body = js_function_body(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
"markLocalFolderWatchApplied",
);
assert!(
watch_applied_body.contains("data-mnote-local-folder-watch-applied")
&& watch_applied_body.contains("String(value || 'projection')"),
@@ -826,8 +846,10 @@ mod tests {
assert!(FILETREE_DND_RUNTIME_JS.contains("window.__mnoteFileTreeDndRuntime"));
assert!(FILETREE_KEYBOARD_RUNTIME_JS.contains("window.__mnoteFileTreeKeyboardRuntime"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#)
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"class="tree-link-title" title="' + escapeHtml(title) + '""#));
}
@@ -891,55 +913,55 @@ mod tests {
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("var workspaceId = resolveWorkspaceId(openTrigger);")
);
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"[data-page-title-input="true"][data-document-id="' + escaped + '"]"#));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#".wolai-breadcrumb-current [data-page-title-current]"#));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains(
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(!SIDEBAR_TREE_RUNTIME_JS
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("renderSidebarSnapshot"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("kernel_file_tree_projection"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("deltaNeedsProjectionRefresh"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("upsert_documents"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("renderSidebarSnapshot"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("kernel_file_tree_projection"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("deltaNeedsProjectionRefresh"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("upsert_documents"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setTreeLiveApplyError"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("scheduleProjectionRefresh"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/file"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
}
#[test]
fn sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreePageTitle(title)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("normalizeFileTreePageRenameTitle"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("validateFileTreeRename"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("同级已存在同名页面"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("文件名不能包含"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("rowId === 'doc:' + activeId"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreePageTitle(title)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeFileTreePageRenameTitle"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("validateFileTreeRename"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("同级已存在同名页面"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("文件名不能包含"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'doc:' + activeId"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("currentFileTreeActiveRowId"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("window.location.pathname.match(/^\\/mindmap\\/([^\\/]+)\\/([^\\/]+)/)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
"var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId"
));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileRows('', groupRowsByParent(rows), activeId, activeRowId)"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileRows(nodeId, grouped, activeId, activeRowId)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("normalizeMindmapFileTreeTitle"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shortMindmapFileName(assetId)"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeMindmapFileTreeTitle"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("shortMindmapFileName(assetId)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("var title = isFileTreeProjectionPageRow(rowKind, assetId)"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("title: 'index.md'"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("title: 'index.md'"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
}
#[test]
@@ -1064,12 +1086,12 @@ mod tests {
"tree.asset.open detail 应携带资源 owner document"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-owner-document-id=\"' + escapeHtml(ownerDocumentId)"),
"SSR filetree 行应输出 data-owner-document-id"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("return 'local-md:' + bundleName + '~2F' + bundleName + '.md';"),
"local_folder bundle 资源应从 local-file 路径推导 owner markdown document"
);