From 8c895b3dc0d880a3481d89f98bbcd8bd51ab75d1 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Thu, 30 Apr 2026 06:58:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E6=95=9B=E6=A0=91=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E4=B8=8E=E6=8A=95=E5=BD=B1=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/crates/bridge-runtime/src/lib.rs | 427 ++++++++++-------- rust/crates/core-protocol/src/lib.rs | 2 +- .../mnote-web/src/page_aggregate/builder.rs | 4 +- rust/crates/mnote-web/src/routes/documents.rs | 8 +- rust/crates/mnote-web/src/routes/hermes.rs | 10 +- rust/crates/mnote-web/src/routes/search.rs | 9 +- rust/crates/mnote-web/src/routes/sse.rs | 7 +- rust/crates/mnote-web/src/routes/tree.rs | 38 +- rust/crates/mnote-web/src/routes/web_shell.rs | 20 +- .../mnote-web/src/ssr/pages/document.rs | 10 +- rust/crates/mnote-web/src/ssr/pages/layout.rs | 426 +++++++++++++++-- rust/crates/mnote-web/src/ssr/styles.rs | 235 +++++++++- rust/crates/mnote-web/src/workspace_shell.rs | 26 +- rust/target/.rustc_info.json | 2 +- .../src/lib/documents/rust-runtime.test.ts | 21 + .../src/lib/documents/rust-runtime.ts | 16 +- 16 files changed, 980 insertions(+), 281 deletions(-) diff --git a/rust/crates/bridge-runtime/src/lib.rs b/rust/crates/bridge-runtime/src/lib.rs index 6b4ce965..e0bbebc7 100644 --- a/rust/crates/bridge-runtime/src/lib.rs +++ b/rust/crates/bridge-runtime/src/lib.rs @@ -771,7 +771,7 @@ struct DocumentMoveSnapshotDocument { #[serde(default, alias = "parent_id")] parent_id: Option, #[serde(default, alias = "sort_order")] - sort_order: Option, + sort_order: Option, #[serde(default, alias = "created_at")] created_at: Option, } @@ -904,12 +904,18 @@ fn normalize_parent_id(value: &Option) -> Option { fn document_move_sort_key(document: &DocumentMoveSnapshotDocument) -> (i64, String, String) { ( - document.sort_order.unwrap_or(i64::MAX), + normalize_document_move_sort_order(document.sort_order).unwrap_or(i64::MAX), document.created_at.clone().unwrap_or_default(), document.id.clone(), ) } +fn normalize_document_move_sort_order(value: Option) -> Option { + value + .filter(|number| number.is_finite()) + .map(|number| number.floor() as i64) +} + fn clamp_document_move_index(sort_order: i64, max: usize) -> usize { if sort_order < 0 { return 0; @@ -927,7 +933,7 @@ fn append_document_move_order_patches( let sort_order = index as i64; let moved = document.id == moved_document_id; if normalize_parent_id(&document.parent_id) == parent_id - && document.sort_order == Some(sort_order) + && normalize_document_move_sort_order(document.sort_order) == Some(sort_order) && !moved { continue; @@ -2286,11 +2292,15 @@ fn materialize_tree_domain_event_plans( .map(|event_plans| { event_plans .iter() - .filter_map(|event_plan| materialize_tree_domain_event_value(event_plan, plan, result)) + .filter_map(|event_plan| { + materialize_tree_domain_event_value(event_plan, plan, result) + }) .collect() }) .filter(|event_plans: &Vec<(String, Value)>| !event_plans.is_empty()) - .or_else(|| materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan])) + .or_else(|| { + materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan]) + }) .unwrap_or_default() } @@ -2467,36 +2477,36 @@ pub fn build_runtime_command_artifact_plan( let domain_events: Vec = materialized_event_plans .iter() .enumerate() - .map(|(index, (event_type, domain_event_plan))| { - RuntimeDomainEventArtifactPlan { - workspace_id: workspace_id.into(), - id: tree_domain_event_artifact_id( - &command.command_id, - event_type, - index, - materialized_event_plans.len(), - ), - request_id: context.request_id.clone(), - trace_id: context.trace_id.clone(), - command_id: command.command_id.clone(), - command_log_id: command_log_id.clone(), - event_type: event_type.clone(), - aggregate_type: aggregate_type.into(), - aggregate_id: aggregate_id.into(), - event_version: 1, - status: "committed".into(), - actor_type: context.actor.actor_type.clone(), - payload: tree_artifact_payload( - context, - command, - &event_type, - aggregate_type, - aggregate_id, - &domain_event_plan, - ), - created_at: now.into(), - } - }) + .map( + |(index, (event_type, domain_event_plan))| RuntimeDomainEventArtifactPlan { + workspace_id: workspace_id.into(), + id: tree_domain_event_artifact_id( + &command.command_id, + event_type, + index, + materialized_event_plans.len(), + ), + request_id: context.request_id.clone(), + trace_id: context.trace_id.clone(), + command_id: command.command_id.clone(), + command_log_id: command_log_id.clone(), + event_type: event_type.clone(), + aggregate_type: aggregate_type.into(), + aggregate_id: aggregate_id.into(), + event_version: 1, + status: "committed".into(), + actor_type: context.actor.actor_type.clone(), + payload: tree_artifact_payload( + context, + command, + &event_type, + aggregate_type, + aggregate_id, + &domain_event_plan, + ), + created_at: now.into(), + }, + ) .collect(); let domain_event = domain_events.first().cloned(); @@ -2507,7 +2517,12 @@ pub fn build_runtime_command_artifact_plan( }) } -fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usize, total: usize) -> String { +fn tree_domain_event_artifact_id( + command_id: &str, + event_type: &str, + index: usize, + total: usize, +) -> String { if total <= 1 || index == 0 { return format!("evt_{command_id}"); } @@ -2524,7 +2539,10 @@ fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usiz format!("evt_{command_id}_{:02}_{suffix}", index + 1) } -fn read_preflight_field<'a>(command: &'a RuntimeCommandEnvelopeWire, field: &str) -> Option<&'a Value> { +fn read_preflight_field<'a>( + command: &'a RuntimeCommandEnvelopeWire, + field: &str, +) -> Option<&'a Value> { command .preflight_data .as_ref() @@ -5168,7 +5186,9 @@ fn build_page_aggregate_projection_result( source: PageAggregateSource, ) -> Result { if data.is_null() { - return Err(BridgeError::not_found("page.aggregate.get 未返回页面聚合数据")); + return Err(BridgeError::not_found( + "page.aggregate.get 未返回页面聚合数据", + )); } let meta = data.get("meta").unwrap_or(data); @@ -5248,9 +5268,8 @@ fn build_page_aggregate_projection_result( .cloned() .unwrap_or(Value::Null), }; - let layout_options = serde_json::to_value(&page_options).map_err(|error| { - BridgeError::transport(format!("PageOptions 序列化失败: {error}")) - })?; + let layout_options = serde_json::to_value(&page_options) + .map_err(|error| BridgeError::transport(format!("PageOptions 序列化失败: {error}")))?; let revision_ref = revision .as_u64() .map(|value| format!("{resolved_document_id}:{value}")); @@ -5275,7 +5294,9 @@ fn build_page_aggregate_projection_result( title, updated_at: updated_at.map(Value::String).unwrap_or(Value::Null), permissions: PagePermissions { - read_only: bool_field(meta, "can_edit").map(|can_edit| !can_edit).unwrap_or(false), + read_only: bool_field(meta, "can_edit") + .map(|can_edit| !can_edit) + .unwrap_or(false), disable_download: bool_field(meta, "disable_download") .or_else(|| bool_field(meta, "disableDownload")) .unwrap_or(false), @@ -5353,11 +5374,7 @@ fn build_mindmap_projection_result( .filter(|value| value.get("data").is_some() || value.get("children").is_some()) .unwrap_or(data); let tree = normalize_mindmap_from_value(tree_input)?; - let root_node = tree - .data - .uid - .clone() - .unwrap_or_else(|| "root".into()); + let root_node = tree.data.uid.clone().unwrap_or_else(|| "root".into()); let mut nodes = Vec::new(); let mut edges = Vec::new(); collect_mindmap_projection_rows(&tree, None, &mut nodes, &mut edges); @@ -5414,10 +5431,12 @@ fn collect_mindmap_projection_rows( } fn search_documents_canonical_projection(evaluation: SearchDocumentsEvaluation) -> Value { - let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| json!({ - "enqueueAssetIds": [], - "results": [] - })); + let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| { + json!({ + "enqueueAssetIds": [], + "results": [] + }) + }); if let Some(map) = payload.as_object_mut() { map.insert("projectionOwner".into(), json!("rust-kernel")); if let Some(results) = map.get_mut("results").and_then(Value::as_array_mut) { @@ -7483,9 +7502,7 @@ fn execute_query_result( let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?; let result = build_mindmap_projection_result(&data, &payload.mindmap_id)?; serde_json::to_value(result).map_err(|error| { - BridgeError::transport(format!( - "mindmap.projection.get result 序列化失败: {error}" - )) + BridgeError::transport(format!("mindmap.projection.get result 序列化失败: {error}")) }) } "search.documents" | "search.documents.query" => { @@ -8167,14 +8184,10 @@ fn execute_command( "documentId": payload.document_id, }), ); - let page_body_event_payload = document_save_page_body_domain_event_payload( - &payload, - &editor_document, - ); - let snapshot_event_payload = document_save_snapshot_domain_event_payload( - &payload, - &canonical_content, - )?; + let page_body_event_payload = + document_save_page_body_domain_event_payload(&payload, &editor_document); + let snapshot_event_payload = + document_save_snapshot_domain_event_payload(&payload, &canonical_content)?; let page_body_event_plan = tree_domain_event_plan_with_payload( "page.body.saved", page_body_event_payload, @@ -8207,7 +8220,8 @@ fn execute_command( } else { "documents.embed" }; - let page_aggregate_embed_plan = build_page_aggregate_embed_plan(&command_wire, &payload)?; + let page_aggregate_embed_plan = + build_page_aggregate_embed_plan(&command_wire, &payload)?; let embed_content = page_aggregate_embed_plan .as_ref() .and_then(|plan| plan.get("content")) @@ -9575,47 +9589,47 @@ mod tests { "id": "block_1", "type": "paragraph", }, - "streamDeltaHint": { - "family": "tree", - "kind": "resync_required", - "args": { - "reason": "blocks.patch", - "documentId": "doc_1", - "blockId": "block_1" - } - }, - "domainEventHint": { - "family": "tree", - "eventType": "block.patched" - }, - "domainEventPlan": { - "family": "tree", - "schema": "mnote.tree.domain_event", - "schemaVersion": 1, - "eventType": "block.patched", - "payload": { - "document": { - "id": "doc_1", - "workspaceId": "ws_1" - }, - "block": { - "id": "block_1" - }, - "patch": { - "summary": "replace_block", - "nextType": "paragraph" - } - }, - "streamDeltaHint": { - "family": "tree", - "kind": "resync_required", - "args": { - "reason": "blocks.patch", - "documentId": "doc_1", - "blockId": "block_1" - } - } - } + "streamDeltaHint": { + "family": "tree", + "kind": "resync_required", + "args": { + "reason": "blocks.patch", + "documentId": "doc_1", + "blockId": "block_1" + } + }, + "domainEventHint": { + "family": "tree", + "eventType": "block.patched" + }, + "domainEventPlan": { + "family": "tree", + "schema": "mnote.tree.domain_event", + "schemaVersion": 1, + "eventType": "block.patched", + "payload": { + "document": { + "id": "doc_1", + "workspaceId": "ws_1" + }, + "block": { + "id": "block_1" + }, + "patch": { + "summary": "replace_block", + "nextType": "paragraph" + } + }, + "streamDeltaHint": { + "family": "tree", + "kind": "resync_required", + "args": { + "reason": "blocks.patch", + "documentId": "doc_1", + "blockId": "block_1" + } + } + } }) ); } @@ -9775,7 +9789,10 @@ mod tests { .expect("canonical search query result should build"); assert_eq!(result["projectionOwner"], json!("rust-kernel")); - assert_eq!(result["results"][0]["projectionOwner"], json!("rust-kernel")); + assert_eq!( + result["results"][0]["projectionOwner"], + json!("rust-kernel") + ); assert_eq!(result["results"][0]["id"], json!("page_1")); } @@ -9848,7 +9865,10 @@ mod tests { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "mindmap.command.apply"); assert_eq!(plan.function_name, "mindmaps:applyCommand"); - assert_eq!(plan.args_json["canonicalCommand"], json!("mindmap.command.apply")); + assert_eq!( + plan.args_json["canonicalCommand"], + json!("mindmap.command.apply") + ); } RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => { panic!("expected command plan") @@ -11514,11 +11534,11 @@ mod tests { "content": [{ "id": "block_1", "type": "pageReference" }], "expectedRevision": 5, "conflictDetectionKey": "conflict_5", - "sourceDocumentId": "doc_1", - "targetDocumentId": "doc_2", - "anchorBlockId": "anchor_1", - "pageAggregateEmbedPlan": null, - "streamDeltaHint": { + "sourceDocumentId": "doc_1", + "targetDocumentId": "doc_2", + "anchorBlockId": "anchor_1", + "pageAggregateEmbedPlan": null, + "streamDeltaHint": { "family": "tree", "kind": "noop", "args": {} @@ -12009,6 +12029,31 @@ mod tests { ); } + #[test] + fn document_move_order_plan_accepts_convex_float_sort_order_snapshot() { + let payload = DocumentMoveCommandPayload { + document_id: "doc_b".into(), + parent_id: None, + sort_order: 0, + }; + let snapshot = serde_json::from_value::(json!({ + "documents": [ + { "id": "doc_a", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0.0, "created_at": "2026-04-25T00:00:01Z" }, + { "id": "doc_b", "workspace_id": "ws_1", "parent_id": null, "sort_order": 1.0, "created_at": "2026-04-25T00:00:02Z" } + ] + })) + .expect("snapshot with float sort_order"); + + let plan = build_document_move_order_plan_from_snapshot(&payload, &snapshot) + .expect("move order plan"); + + assert_eq!(plan.normalized_sort_order, 0); + assert!(plan + .patches + .iter() + .any(|patch| patch.document_id == "doc_b" && patch.sort_order == 0 && patch.moved)); + } + #[test] fn document_move_order_plan_normalizes_cross_parent_and_clamps_index() { let payload = DocumentMoveCommandPayload { @@ -12598,33 +12643,33 @@ mod tests { page_id: Some("doc_2".into()), block_id: None, }), - payload: json!({ - "documentId": "doc_2", - "workspaceId": "ws_1", - "revision": 5, - "conflictDetectionKey": "conflict_5", - "sourceDocumentId": "doc_1", - "targetDocumentId": "doc_2", - "anchorBlockId": "anchor_1", - }), - preflight_data: Some(json!({ - "pageAggregateEmbed": { - "sourceDocumentId": "doc_1", - "sourceTitle": "来源页面", - "targetDocumentId": "doc_2", - "targetContent": { - "blocks": [ - { "id": "anchor_1", "type": "paragraph" } - ], - "format": "editor" - }, - "anchorBlockId": "anchor_1", - "blockId": "page_ref_doc_1" - } - })), - reason: Some("树命令嵌入页面".into()), - refs: vec![], - dry_run: false, + payload: json!({ + "documentId": "doc_2", + "workspaceId": "ws_1", + "revision": 5, + "conflictDetectionKey": "conflict_5", + "sourceDocumentId": "doc_1", + "targetDocumentId": "doc_2", + "anchorBlockId": "anchor_1", + }), + preflight_data: Some(json!({ + "pageAggregateEmbed": { + "sourceDocumentId": "doc_1", + "sourceTitle": "来源页面", + "targetDocumentId": "doc_2", + "targetContent": { + "blocks": [ + { "id": "anchor_1", "type": "paragraph" } + ], + "format": "editor" + }, + "anchorBlockId": "anchor_1", + "blockId": "page_ref_doc_1" + } + })), + reason: Some("树命令嵌入页面".into()), + refs: vec![], + dry_run: false, validate_only: false, }, }) @@ -12636,59 +12681,59 @@ mod tests { assert_eq!(plan.command_name, "tree.node.embed"); assert_eq!( plan.args_json, - json!({ - "id": "doc_2", - "content": { - "blocks": [ - { "id": "anchor_1", "type": "paragraph" }, - { - "id": "page_ref_doc_1", - "type": "pageReference", - "props": { - "pageId": "doc_1", - "title": "来源页面" - } - } - ], - "format": "editor" - }, - "expectedRevision": 5, - "conflictDetectionKey": "conflict_5", - "sourceDocumentId": "doc_1", - "targetDocumentId": "doc_2", - "anchorBlockId": "anchor_1", - "pageAggregateEmbedPlan": { - "schema": "mnote.page_aggregate.embed_plan", - "schemaVersion": 1, - "sourceDocumentId": "doc_1", - "targetDocumentId": "doc_2", - "anchorBlockId": "anchor_1", - "insertIndex": 1, - "block": { - "id": "page_ref_doc_1", - "type": "pageReference", - "props": { - "pageId": "doc_1", - "title": "来源页面" - } - }, - "content": { - "blocks": [ - { "id": "anchor_1", "type": "paragraph" }, - { - "id": "page_ref_doc_1", - "type": "pageReference", - "props": { - "pageId": "doc_1", - "title": "来源页面" - } - } - ], - "format": "editor" - }, - "blockCount": 2 - }, - "streamDeltaHint": { + json!({ + "id": "doc_2", + "content": { + "blocks": [ + { "id": "anchor_1", "type": "paragraph" }, + { + "id": "page_ref_doc_1", + "type": "pageReference", + "props": { + "pageId": "doc_1", + "title": "来源页面" + } + } + ], + "format": "editor" + }, + "expectedRevision": 5, + "conflictDetectionKey": "conflict_5", + "sourceDocumentId": "doc_1", + "targetDocumentId": "doc_2", + "anchorBlockId": "anchor_1", + "pageAggregateEmbedPlan": { + "schema": "mnote.page_aggregate.embed_plan", + "schemaVersion": 1, + "sourceDocumentId": "doc_1", + "targetDocumentId": "doc_2", + "anchorBlockId": "anchor_1", + "insertIndex": 1, + "block": { + "id": "page_ref_doc_1", + "type": "pageReference", + "props": { + "pageId": "doc_1", + "title": "来源页面" + } + }, + "content": { + "blocks": [ + { "id": "anchor_1", "type": "paragraph" }, + { + "id": "page_ref_doc_1", + "type": "pageReference", + "props": { + "pageId": "doc_1", + "title": "来源页面" + } + } + ], + "format": "editor" + }, + "blockCount": 2 + }, + "streamDeltaHint": { "family": "tree", "kind": "noop", "args": {} diff --git a/rust/crates/core-protocol/src/lib.rs b/rust/crates/core-protocol/src/lib.rs index 1fb98566..74f60f2c 100644 --- a/rust/crates/core-protocol/src/lib.rs +++ b/rust/crates/core-protocol/src/lib.rs @@ -1,6 +1,6 @@ +pub mod ai; pub mod command; pub mod common; -pub mod ai; pub mod editor; pub mod governance; pub mod kernel; diff --git a/rust/crates/mnote-web/src/page_aggregate/builder.rs b/rust/crates/mnote-web/src/page_aggregate/builder.rs index abcfd9b1..c5f8c15b 100644 --- a/rust/crates/mnote-web/src/page_aggregate/builder.rs +++ b/rust/crates/mnote-web/src/page_aggregate/builder.rs @@ -331,9 +331,7 @@ impl PageAggregateBuilder { disable_copy: self.disable_copy, }, }, - layout: PageLayout { - page_options, - }, + layout: PageLayout { page_options }, body: PageBody { content: self.content, revision: self.revision, diff --git a/rust/crates/mnote-web/src/routes/documents.rs b/rust/crates/mnote-web/src/routes/documents.rs index c518fc0c..7a768b39 100644 --- a/rust/crates/mnote-web/src/routes/documents.rs +++ b/rust/crates/mnote-web/src/routes/documents.rs @@ -567,8 +567,7 @@ pub async fn title( let title = body.title.trim(); if title.is_empty() { return Err( - WebError::bad_request_code("title_required", "缺少有效页面标题") - .with_context(&context), + WebError::bad_request_code("title_required", "缺少有效页面标题").with_context(&context), ); } let effective_workspace_id = @@ -974,6 +973,9 @@ mod tests { .expect("body"); let payload: Value = serde_json::from_slice(&body).expect("json"); assert_eq!(payload["meta"]["commandName"], "page.layout.updateOptions"); - assert_eq!(payload["meta"]["canonicalCommand"], "page.layout.updateOptions"); + assert_eq!( + payload["meta"]["canonicalCommand"], + "page.layout.updateOptions" + ); } } diff --git a/rust/crates/mnote-web/src/routes/hermes.rs b/rust/crates/mnote-web/src/routes/hermes.rs index cc648d08..37cf805f 100644 --- a/rust/crates/mnote-web/src/routes/hermes.rs +++ b/rust/crates/mnote-web/src/routes/hermes.rs @@ -252,7 +252,10 @@ mod tests { assert_eq!(payload["contract"]["toolEventOwner"], "rust-web-hermes"); assert_eq!(payload["contract"]["clientActionOwner"], "rust-web-hermes"); assert_eq!(payload["canonicalRoute"], "/api/hermes/bridge"); - assert!(payload["eventStreamEndpoint"].as_str().unwrap_or_default().contains("/api/hermes/events/")); + assert!(payload["eventStreamEndpoint"] + .as_str() + .unwrap_or_default() + .contains("/api/hermes/events/")); } #[tokio::test] @@ -284,6 +287,9 @@ mod tests { let payload: Value = serde_json::from_slice(&body).expect("json"); assert_eq!(payload["bridge"], "hermes_session"); assert_eq!(payload["canonicalRoute"], "/api/hermes/bridge"); - assert_eq!(payload["contract"]["structuredWriteOwner"], "rust-web-hermes"); + assert_eq!( + payload["contract"]["structuredWriteOwner"], + "rust-web-hermes" + ); } } diff --git a/rust/crates/mnote-web/src/routes/search.rs b/rust/crates/mnote-web/src/routes/search.rs index 1b4c2059..9599873e 100644 --- a/rust/crates/mnote-web/src/routes/search.rs +++ b/rust/crates/mnote-web/src/routes/search.rs @@ -230,7 +230,14 @@ async fn load_search_results_with_filters( "customRangeTo": filters.custom_range.as_ref().and_then(|range| range.to.clone()), }), }; - match execute_runtime_query_via_convex(config, context, Some(workspace_id), runtime_query.clone()).await { + match execute_runtime_query_via_convex( + config, + context, + Some(workspace_id), + runtime_query.clone(), + ) + .await + { Ok(value) => Ok(value), Err(_) => execute_runtime_query_against_data( context, diff --git a/rust/crates/mnote-web/src/routes/sse.rs b/rust/crates/mnote-web/src/routes/sse.rs index ca07db7b..e8132745 100644 --- a/rust/crates/mnote-web/src/routes/sse.rs +++ b/rust/crates/mnote-web/src/routes/sse.rs @@ -161,7 +161,12 @@ fn stream_event(event_name: &str, payload: &Value) -> Event { .map(ToOwned::to_owned) .or_else(|| value.as_u64().map(|number| number.to_string())) }) - .or_else(|| payload.get("cursor").and_then(Value::as_str).map(ToOwned::to_owned)) + .or_else(|| { + payload + .get("cursor") + .and_then(Value::as_str) + .map(ToOwned::to_owned) + }) .unwrap_or_else(|| "0".into()); Event::default() .event(event_name) diff --git a/rust/crates/mnote-web/src/routes/tree.rs b/rust/crates/mnote-web/src/routes/tree.rs index e6b33a81..1d257930 100644 --- a/rust/crates/mnote-web/src/routes/tree.rs +++ b/rust/crates/mnote-web/src/routes/tree.rs @@ -4511,6 +4511,37 @@ fn create_command_wire( } } +async fn load_tree_move_preflight_data( + state: &AppState, + context: &RequestContext, + workspace_id: &str, +) -> Result { + let spec = ProjectionSnapshotSpec { + workspace_id, + root_node_id: None, + depth: Some(99), + projection: KernelProjectionKind::SidebarTree, + query: None, + max_results: None, + }; + let snapshot = load_projection_snapshot(state.config(), context, &spec) + .await + .map_err(|error| { + WebError::bad_gateway_code( + "tree_move_preflight_snapshot_failed", + format!("移动前排序快照加载失败: {}", error.message()), + ) + .with_context(context) + .with_header("x-error-phase", "tree_move_preflight_snapshot") + })?; + let documents = snapshot + .dataset + .get("documents") + .cloned() + .unwrap_or_else(|| json!([])); + Ok(json!({ "documents": documents })) +} + async fn resolve_tree_create_workspace_id( state: &AppState, context: &RequestContext, @@ -4671,7 +4702,12 @@ pub async fn tree_command( _ => resolve_effective_workspace_id(&context, requested_workspace_id, true)? .expect("workspace_required 已确保存在"), }; - let command_wire = create_command_wire(&context, &effective_workspace_id, request)?; + let needs_move_preflight = matches!(&request, TreeCommandRequest::Move { .. }); + let mut command_wire = create_command_wire(&context, &effective_workspace_id, request)?; + if needs_move_preflight { + command_wire.preflight_data = + Some(load_tree_move_preflight_data(&state, &context, &effective_workspace_id).await?); + } let execution = execute_runtime_command_via_convex_with_artifacts( state.config(), &context, diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 28d675b7..4fe01e60 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -195,12 +195,12 @@ fn render_document_title_controller_script() -> &'static str { } if (!documentId) return; const escapedId = cssEscape(documentId); - setText(`[data-node-id="${escapedId}"] .tree-link-title`, title); - setText(`[data-document-id="${escapedId}"] .tree-link-title`, title); - setText(`[data-doc-id="${escapedId}"] .tree-link-title`, title); - setText(`[data-node-id="${escapedId}"] .wolai-row-title`, title); - setText(`a[href="/documents/${escapedId}"] .wolai-row-title`, title); - setText(`a[href^="/documents/${escapedId}?"] .wolai-row-title`, title); + setText(`.tree-row[data-node-id="${escapedId}"] > .tree-link > .tree-link-title`, title); + setText(`.tree-row[data-document-id="${escapedId}"] > .tree-link > .tree-link-title`, title); + setText(`.tree-row[data-doc-id="${escapedId}"] > .tree-link > .tree-link-title`, title); + setText(`.wolai-page-row[data-node-id="${escapedId}"] > .wolai-row-title`, title); + setText(`a[href="/documents/${escapedId}"] > .wolai-row-title`, title); + setText(`a[href^="/documents/${escapedId}?"] > .wolai-row-title`, title); }; const saveTitle = async () => { @@ -627,8 +627,9 @@ async fn build_page_aggregate_snapshot( }), )?; - serde_json::from_value::(projection) - .map_err(|error| WebError::internal(format!("Page Aggregate projection 反序列化失败: {error}"))) + serde_json::from_value::(projection).map_err(|error| { + WebError::internal(format!("Page Aggregate projection 反序列化失败: {error}")) + }) } fn stamp_recent_page_cookie(headers: &mut HeaderMap, document_id: &str) { @@ -941,6 +942,9 @@ mod tests { assert!(html.contains("data-page-title-input=\"true\"")); assert!(html.contains("data-title-endpoint=\"/api/documents/title\"")); assert!(html.contains("mnote.document_title_controller.v1")); + assert!(html + .contains(".tree-row[data-node-id=\"${escapedId}\"] > .tree-link > .tree-link-title")); + assert!(!html.contains("[data-node-id=\"${escapedId}\"] .tree-link-title")); assert!(html.contains("\"titleEndpoint\":\"/api/documents/title\"")); assert!(html.contains("__MNOTE_TREE_LIVE_BOOTSTRAP__")); assert!(html.contains("mnote.tree_live_bootstrap.v1")); diff --git a/rust/crates/mnote-web/src/ssr/pages/document.rs b/rust/crates/mnote-web/src/ssr/pages/document.rs index ca24febd..4d539c7f 100644 --- a/rust/crates/mnote-web/src/ssr/pages/document.rs +++ b/rust/crates/mnote-web/src/ssr/pages/document.rs @@ -48,13 +48,9 @@ pub fn DocumentPage(
- +