20260513 mindmap优化01
This commit is contained in:
@@ -13,10 +13,11 @@ use core_protocol::{
|
||||
DocumentReadSubtree, EditorBlock, EditorBlockDocument, EditorBlockType, EditorCommand,
|
||||
EditorInsertBlockAfter, EditorReplaceBlock, EmbedBlock, GetBlock, GetBridgeCommand,
|
||||
GetBridgeRequest, GetBridgeTrace, GetMindmap, InvocationKind, KernelAttachEdge,
|
||||
KernelAuditStamp, KernelContentPayload, KernelCreateNode, KernelDetachEdge, KernelEdge,
|
||||
KernelEdgeListResult, KernelEdgeType, KernelGetNode, KernelGetSubtree, KernelGraphDirection,
|
||||
KernelGraphTraversalResult, KernelGraphVisit, KernelListChildren, KernelListEdges,
|
||||
KernelMoveSubtree, KernelNode, KernelNodeMetadata, KernelNodeType, KernelProjectionAssetKind,
|
||||
KernelAuditStamp, KernelBlockAssetRelation, KernelContentPayload, KernelCreateNode,
|
||||
KernelDetachEdge, KernelEdge, KernelEdgeListResult, KernelEdgeType, KernelGetNode,
|
||||
KernelGetSubtree, KernelGraphDirection, KernelGraphTraversalResult, KernelGraphVisit,
|
||||
KernelListChildren, KernelListEdges, KernelMoveSubtree, KernelNode, KernelNodeMetadata,
|
||||
KernelNodeType, KernelObjectIdentity, KernelObjectKind, KernelProjectionAssetKind,
|
||||
KernelProjectionCapability, KernelProjectionFilter, KernelProjectionItem, KernelProjectionKind,
|
||||
KernelProjectionRequest, KernelProjectionResourceKind, KernelProjectionResourceMeta,
|
||||
KernelProjectionResult, KernelProjectionRowKind, KernelRefsPayload, KernelSubtreeRef,
|
||||
@@ -5547,7 +5548,7 @@ fn build_mindmap_kernel_projection_result(
|
||||
associative_lines: collect_mindmap_associative_lines(data),
|
||||
layout: read_mindmap_value_field(data, &["layout", "layoutHints", "layout_hints"])
|
||||
.unwrap_or_else(|| json!("logicalStructure")),
|
||||
theme: read_mindmap_value_field(data, &["theme"]).unwrap_or_else(|| json!("classic")),
|
||||
theme: read_mindmap_value_field(data, &["theme"]).unwrap_or_else(|| json!("default")),
|
||||
view: read_mindmap_value_field(data, &["view"]).unwrap_or_else(|| json!({})),
|
||||
capabilities: MindmapKernelCapabilities {
|
||||
can_edit: true,
|
||||
@@ -5564,6 +5565,45 @@ fn build_mindmap_kernel_projection_result(
|
||||
})
|
||||
}
|
||||
|
||||
fn default_mindmap_theme_config() -> Value {
|
||||
json!({
|
||||
"lineColor": "#7aa2ff",
|
||||
"lineStyle": "curve",
|
||||
"rootLineKeepSameInCurve": true,
|
||||
"rootLineStartPositionKeepSameInCurve": true,
|
||||
"generalizationLineColor": "#ef6a5b",
|
||||
"backgroundColor": "#f6f8fc",
|
||||
"root": {
|
||||
"fillColor": "#e25563",
|
||||
"color": "#ffffff",
|
||||
"fontWeight": "bold",
|
||||
"borderColor": "transparent",
|
||||
"borderWidth": 0,
|
||||
"borderRadius": 8
|
||||
},
|
||||
"second": {
|
||||
"fillColor": "#4f7df3",
|
||||
"color": "#ffffff",
|
||||
"borderColor": "transparent",
|
||||
"borderWidth": 0,
|
||||
"borderRadius": 8
|
||||
},
|
||||
"node": {
|
||||
"fillColor": "transparent",
|
||||
"color": "#315aa9",
|
||||
"borderColor": "transparent",
|
||||
"borderWidth": 0
|
||||
},
|
||||
"generalization": {
|
||||
"fillColor": "#ffffff",
|
||||
"color": "#ef6a5b",
|
||||
"borderColor": "#ef6a5b",
|
||||
"borderWidth": 1,
|
||||
"borderRadius": 8
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn build_mindmap_adapter_projection_result(
|
||||
data: &Value,
|
||||
mindmap_id: &str,
|
||||
@@ -5577,9 +5617,9 @@ fn build_mindmap_adapter_projection_result(
|
||||
})?,
|
||||
layout: read_mindmap_value_field(data, &["layout", "layoutHints", "layout_hints"])
|
||||
.unwrap_or_else(|| json!("logicalStructure")),
|
||||
theme: read_mindmap_value_field(data, &["theme"]).unwrap_or_else(|| json!("classic")),
|
||||
theme: read_mindmap_value_field(data, &["theme"]).unwrap_or_else(|| json!("default")),
|
||||
theme_config: read_mindmap_value_field(data, &["themeConfig", "theme_config"])
|
||||
.unwrap_or_else(|| json!({})),
|
||||
.unwrap_or_else(default_mindmap_theme_config),
|
||||
view: read_mindmap_value_field(data, &["view"]).unwrap_or_else(|| json!({})),
|
||||
config: read_mindmap_value_field(data, &["config"]).unwrap_or_else(|| json!({})),
|
||||
compat_payload: read_mindmap_value_field(data, &["compatPayload", "compat_payload"])
|
||||
@@ -7075,6 +7115,33 @@ fn is_book_asset(file_name: &str, mime_type: &str) -> bool {
|
||||
.any(|suffix| lowered_name.ends_with(suffix))
|
||||
}
|
||||
|
||||
fn is_onlyoffice_asset(file_name: &str, mime_type: &str) -> bool {
|
||||
let lowered_mime = mime_type.trim().to_ascii_lowercase();
|
||||
let lowered_name = file_name.trim().to_ascii_lowercase();
|
||||
lowered_mime.contains("officedocument")
|
||||
|| lowered_mime.contains("msword")
|
||||
|| lowered_mime.contains("ms-excel")
|
||||
|| lowered_mime.contains("ms-powerpoint")
|
||||
|| [
|
||||
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp",
|
||||
]
|
||||
.iter()
|
||||
.any(|suffix| lowered_name.ends_with(suffix))
|
||||
}
|
||||
|
||||
fn is_code_asset(file_name: &str, mime_type: &str) -> bool {
|
||||
let lowered_mime = mime_type.trim().to_ascii_lowercase();
|
||||
let lowered_name = file_name.trim().to_ascii_lowercase();
|
||||
lowered_mime.starts_with("text/")
|
||||
&& [
|
||||
".c", ".cc", ".cpp", ".css", ".go", ".h", ".hpp", ".html", ".java", ".js",
|
||||
".jsx", ".json", ".kt", ".lua", ".md", ".py", ".rs", ".sh", ".sql", ".toml",
|
||||
".ts", ".tsx", ".xml", ".yaml", ".yml",
|
||||
]
|
||||
.iter()
|
||||
.any(|suffix| lowered_name.ends_with(suffix))
|
||||
}
|
||||
|
||||
fn classify_asset_kind(
|
||||
asset_type: &str,
|
||||
file_name: &str,
|
||||
@@ -7111,7 +7178,17 @@ fn classify_asset_kind(
|
||||
|
||||
fn classify_asset_resource_kind(
|
||||
asset_kind: &KernelProjectionAssetKind,
|
||||
file_name: &str,
|
||||
mime_type: &str,
|
||||
) -> KernelProjectionResourceKind {
|
||||
if matches!(asset_kind, KernelProjectionAssetKind::File) {
|
||||
if is_onlyoffice_asset(file_name, mime_type) {
|
||||
return KernelProjectionResourceKind::OnlyOffice;
|
||||
}
|
||||
if is_code_asset(file_name, mime_type) {
|
||||
return KernelProjectionResourceKind::Code;
|
||||
}
|
||||
}
|
||||
match asset_kind {
|
||||
KernelProjectionAssetKind::Mindmap => KernelProjectionResourceKind::Mindmap,
|
||||
KernelProjectionAssetKind::Table => KernelProjectionResourceKind::Table,
|
||||
@@ -7403,6 +7480,7 @@ fn make_projection_resource_meta(
|
||||
resource_kind: KernelProjectionResourceKind,
|
||||
document_id: Option<String>,
|
||||
asset_id: Option<String>,
|
||||
block_id: Option<String>,
|
||||
workspace_id: Option<String>,
|
||||
asset_kind: Option<KernelProjectionAssetKind>,
|
||||
icon_hint: &str,
|
||||
@@ -7418,6 +7496,34 @@ fn make_projection_resource_meta(
|
||||
) {
|
||||
extra.insert("source".into(), source);
|
||||
}
|
||||
let object_kind = match (&resource_kind, &asset_kind, asset_id.as_ref()) {
|
||||
(KernelProjectionResourceKind::Document, _, _) => KernelObjectKind::Page,
|
||||
(KernelProjectionResourceKind::Index, _, _) => KernelObjectKind::Index,
|
||||
(KernelProjectionResourceKind::Mindmap, _, _) => KernelObjectKind::Mindmap,
|
||||
(KernelProjectionResourceKind::OnlyOffice, _, _) => KernelObjectKind::OnlyOffice,
|
||||
(KernelProjectionResourceKind::Code, _, _) => KernelObjectKind::Code,
|
||||
(_, Some(KernelProjectionAssetKind::Mindmap), _) => KernelObjectKind::Mindmap,
|
||||
(_, _, Some(_)) => KernelObjectKind::Attachment,
|
||||
_ => KernelObjectKind::Page,
|
||||
};
|
||||
let object_identity = Some(KernelObjectIdentity {
|
||||
object_kind,
|
||||
document_id: document_id.clone(),
|
||||
block_id: block_id.clone(),
|
||||
asset_id: asset_id.clone(),
|
||||
});
|
||||
let block_asset_relation =
|
||||
match (document_id.clone(), block_id, asset_id.clone(), asset_kind.clone()) {
|
||||
(Some(document_id), Some(block_id), Some(asset_id), Some(asset_kind)) => {
|
||||
Some(KernelBlockAssetRelation {
|
||||
document_id,
|
||||
block_id,
|
||||
asset_id,
|
||||
asset_kind,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
KernelProjectionResourceMeta {
|
||||
resource_kind: Some(resource_kind),
|
||||
document_id,
|
||||
@@ -7425,6 +7531,8 @@ fn make_projection_resource_meta(
|
||||
workspace_id,
|
||||
asset_kind,
|
||||
icon_hint: Some(icon_hint.into()),
|
||||
object_identity,
|
||||
block_asset_relation,
|
||||
extra,
|
||||
}
|
||||
}
|
||||
@@ -7433,6 +7541,7 @@ fn make_projection_resource_meta(
|
||||
struct NormalizedFileTreeAsset {
|
||||
id: String,
|
||||
document_id: String,
|
||||
block_id: Option<String>,
|
||||
workspace_id: Option<String>,
|
||||
title: String,
|
||||
resource_kind: KernelProjectionResourceKind,
|
||||
@@ -7451,7 +7560,7 @@ fn infer_file_tree_asset_shape(
|
||||
) {
|
||||
let asset_kind = classify_asset_kind(asset_type, file_name, mime_type);
|
||||
(
|
||||
classify_asset_resource_kind(&asset_kind),
|
||||
classify_asset_resource_kind(&asset_kind, file_name, mime_type),
|
||||
asset_kind.clone(),
|
||||
classify_asset_icon_hint(&asset_kind),
|
||||
)
|
||||
@@ -7472,6 +7581,7 @@ fn normalize_file_tree_asset(value: &Value) -> Option<NormalizedFileTreeAsset> {
|
||||
Some(NormalizedFileTreeAsset {
|
||||
id,
|
||||
document_id,
|
||||
block_id: string_field(value, "block_id").or_else(|| string_field(value, "blockId")),
|
||||
workspace_id: string_field(value, "workspace_id")
|
||||
.or_else(|| string_field(value, "workspaceId")),
|
||||
title: file_name,
|
||||
@@ -7651,6 +7761,7 @@ fn build_file_tree_projection_result(
|
||||
KernelProjectionResourceKind::Document,
|
||||
Some(node.id.clone()),
|
||||
None,
|
||||
None,
|
||||
workspace_id.clone(),
|
||||
None,
|
||||
"page",
|
||||
@@ -7682,6 +7793,7 @@ fn build_file_tree_projection_result(
|
||||
KernelProjectionResourceKind::Index,
|
||||
Some(node.id.clone()),
|
||||
None,
|
||||
None,
|
||||
workspace_id.clone(),
|
||||
None,
|
||||
"index",
|
||||
@@ -7728,6 +7840,7 @@ fn build_file_tree_projection_result(
|
||||
asset.resource_kind.clone(),
|
||||
Some(asset.document_id.clone()),
|
||||
Some(asset.id.clone()),
|
||||
asset.block_id.clone(),
|
||||
asset.workspace_id.clone(),
|
||||
Some(KernelProjectionAssetKind::Mindmap),
|
||||
"mindmap",
|
||||
@@ -7768,6 +7881,7 @@ fn build_file_tree_projection_result(
|
||||
child_asset.resource_kind.clone(),
|
||||
Some(child_asset.document_id.clone()),
|
||||
Some(child_asset.id.clone()),
|
||||
child_asset.block_id.clone(),
|
||||
child_asset.workspace_id.clone(),
|
||||
Some(child_asset.asset_kind.clone()),
|
||||
child_asset.icon_hint,
|
||||
@@ -7808,6 +7922,7 @@ fn build_file_tree_projection_result(
|
||||
asset.resource_kind.clone(),
|
||||
Some(asset.document_id.clone()),
|
||||
Some(asset.id.clone()),
|
||||
asset.block_id.clone(),
|
||||
asset.workspace_id.clone(),
|
||||
Some(asset.asset_kind.clone()),
|
||||
asset.icon_hint,
|
||||
@@ -8141,6 +8256,7 @@ fn build_kernel_projection_result(
|
||||
KernelProjectionResourceKind::Document,
|
||||
Some(node.id.clone()),
|
||||
None,
|
||||
None,
|
||||
node.workspace_id.clone(),
|
||||
None,
|
||||
"page",
|
||||
@@ -9189,10 +9305,24 @@ fn execute_command(
|
||||
source: workspace_source_value(&command_wire.source),
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"docId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
"docId": payload.document_id.clone(),
|
||||
"mindmapId": payload.mindmap_id.clone(),
|
||||
"data": payload.data,
|
||||
"createOnly": payload.create_only.unwrap_or(false),
|
||||
"streamDeltaHint": tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.put",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
"domainEventHint": tree_domain_event_hint("tree.resource.mindmap.put"),
|
||||
"domainEventPlan": tree_domain_event_plan(
|
||||
"tree.resource.mindmap.put",
|
||||
tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.put",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -9229,11 +9359,25 @@ fn execute_command(
|
||||
source: workspace_source_value(&command_wire.source),
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"documentId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
"documentId": payload.document_id.clone(),
|
||||
"mindmapId": payload.mindmap_id.clone(),
|
||||
"commands": payload.commands,
|
||||
"projectionRevision": payload.projection_revision,
|
||||
"canonicalCommand": "mindmap.command.apply",
|
||||
"streamDeltaHint": tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.command.apply",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
"domainEventHint": tree_domain_event_hint("tree.resource.mindmap.updated"),
|
||||
"domainEventPlan": tree_domain_event_plan(
|
||||
"tree.resource.mindmap.updated",
|
||||
tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.command.apply",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -9265,8 +9409,22 @@ fn execute_command(
|
||||
source: workspace_source_value(&command_wire.source),
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"docId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
"docId": payload.document_id.clone(),
|
||||
"mindmapId": payload.mindmap_id.clone(),
|
||||
"streamDeltaHint": tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.delete",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
"domainEventHint": tree_domain_event_hint("tree.resource.mindmap.deleted"),
|
||||
"domainEventPlan": tree_domain_event_plan(
|
||||
"tree.resource.mindmap.deleted",
|
||||
tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.delete",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -9299,8 +9457,22 @@ fn execute_command(
|
||||
source: workspace_source_value(&command_wire.source),
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"docId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
"docId": payload.document_id.clone(),
|
||||
"mindmapId": payload.mindmap_id.clone(),
|
||||
"streamDeltaHint": tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.restore",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
"domainEventHint": tree_domain_event_hint("tree.resource.mindmap.restored"),
|
||||
"domainEventPlan": tree_domain_event_plan(
|
||||
"tree.resource.mindmap.restored",
|
||||
tree_stream_delta_hint("resync_required", json!({
|
||||
"reason": "mindmap.restore",
|
||||
"documentId": payload.document_id.clone(),
|
||||
"blockId": payload.mindmap_id.clone(),
|
||||
})),
|
||||
),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -10036,6 +10208,7 @@ where
|
||||
|
||||
fn normalize_editor_block_type_for_save(raw_type: &str) -> EditorBlockType {
|
||||
match raw_type {
|
||||
"mindmap" => EditorBlockType::Mindmap,
|
||||
"heading" => EditorBlockType::Heading,
|
||||
"bullet_list_item" | "bullet_list" | "bullet-list" => EditorBlockType::BulletListItem,
|
||||
"numbered_list_item" | "ordered_list" | "ordered-list" => EditorBlockType::NumberedListItem,
|
||||
@@ -10058,11 +10231,9 @@ fn normalize_editor_block_from_legacy(block: &Value, index: usize) -> EditorBloc
|
||||
let raw_type = read_trimmed_string_field(block, &["blockType", "type"])
|
||||
.unwrap_or_else(|| "paragraph".into())
|
||||
.to_lowercase();
|
||||
let normalized_block_type = normalize_editor_block_type_for_save(&raw_type);
|
||||
let mut props = BlockProps::default();
|
||||
if matches!(
|
||||
normalize_editor_block_type_for_save(&raw_type),
|
||||
EditorBlockType::Heading
|
||||
) {
|
||||
if matches!(normalized_block_type, EditorBlockType::Heading) {
|
||||
props.heading_level = block
|
||||
.get("props")
|
||||
.and_then(Value::as_object)
|
||||
@@ -10076,20 +10247,14 @@ fn normalize_editor_block_from_legacy(block: &Value, index: usize) -> EditorBloc
|
||||
.and_then(|map| map.get("collapsed"))
|
||||
.and_then(Value::as_bool);
|
||||
}
|
||||
if matches!(
|
||||
normalize_editor_block_type_for_save(&raw_type),
|
||||
EditorBlockType::Todo
|
||||
) {
|
||||
if matches!(normalized_block_type, EditorBlockType::Todo) {
|
||||
props.checked = block
|
||||
.get("props")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|map| map.get("checked"))
|
||||
.and_then(Value::as_bool);
|
||||
}
|
||||
if matches!(
|
||||
normalize_editor_block_type_for_save(&raw_type),
|
||||
EditorBlockType::CodeBlock
|
||||
) {
|
||||
if matches!(normalized_block_type, EditorBlockType::CodeBlock) {
|
||||
props.language = block
|
||||
.get("props")
|
||||
.and_then(Value::as_object)
|
||||
@@ -10121,15 +10286,72 @@ fn normalize_editor_block_from_legacy(block: &Value, index: usize) -> EditorBloc
|
||||
{
|
||||
props.extra.insert("tiptapTocNode".into(), tiptap_toc);
|
||||
}
|
||||
if matches!(normalized_block_type, EditorBlockType::Mindmap) {
|
||||
let props_map = block.get("props").and_then(Value::as_object);
|
||||
let legacy_data = props_map.and_then(|map| map.get("data"));
|
||||
let data_object = legacy_data.and_then(Value::as_object);
|
||||
if let Some(mindmap_id) = props_map
|
||||
.and_then(|map| map.get("mindmapId").or_else(|| map.get("mindmap_id")))
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
data_object.and_then(|map| {
|
||||
map.get("mindmapId")
|
||||
.or_else(|| map.get("mindmap_id"))
|
||||
.or_else(|| map.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
props
|
||||
.extra
|
||||
.insert("mindmapId".into(), Value::String(mindmap_id.to_string()));
|
||||
} else {
|
||||
props
|
||||
.extra
|
||||
.insert("mindmapId".into(), Value::String(block_id.clone()));
|
||||
}
|
||||
if let Some(root_node_id) = props_map
|
||||
.and_then(|map| map.get("rootNodeId").or_else(|| map.get("root_node_id")))
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
data_object.and_then(|map| {
|
||||
map.get("rootNodeId")
|
||||
.or_else(|| map.get("root_node_id"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
props
|
||||
.extra
|
||||
.insert("rootNodeId".into(), Value::String(root_node_id.to_string()));
|
||||
}
|
||||
if let Some(projection_version) = props_map
|
||||
.and_then(|map| map.get("projectionVersion"))
|
||||
.and_then(Value::as_u64)
|
||||
{
|
||||
props.extra.insert(
|
||||
"projectionVersion".into(),
|
||||
Value::Number(projection_version.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
let text = read_trimmed_string_field(block, &["content"])
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| extract_inline_text(block));
|
||||
|
||||
EditorBlock {
|
||||
block_id,
|
||||
block_type: normalize_editor_block_type_for_save(&raw_type),
|
||||
block_type: normalized_block_type.clone(),
|
||||
props,
|
||||
content_nodes: build_text_content_nodes(&text),
|
||||
content_nodes: if matches!(normalized_block_type, EditorBlockType::Mindmap) {
|
||||
vec![]
|
||||
} else {
|
||||
build_text_content_nodes(&text)
|
||||
},
|
||||
child_block_ids: vec![],
|
||||
}
|
||||
}
|
||||
@@ -10156,6 +10378,7 @@ fn normalize_save_editor_document(
|
||||
) -> Result<EditorBlockDocument, BridgeError> {
|
||||
if let Some(editor_document) = payload.editor_document.clone() {
|
||||
if let Ok(mut parsed) = serde_json::from_value::<EditorBlockDocument>(editor_document) {
|
||||
hydrate_editor_document_props_from_raw(&mut parsed, payload.editor_document.as_ref());
|
||||
if parsed.document_id.trim().is_empty() {
|
||||
parsed.document_id = payload.document_id.clone();
|
||||
}
|
||||
@@ -10189,6 +10412,80 @@ fn normalize_save_editor_document(
|
||||
))
|
||||
}
|
||||
|
||||
fn hydrate_editor_document_props_from_raw(parsed: &mut EditorBlockDocument, raw: Option<&Value>) {
|
||||
let Some(raw_blocks) = raw
|
||||
.and_then(|value| value.get("blocks"))
|
||||
.and_then(Value::as_array)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for (index, block) in parsed.blocks.iter_mut().enumerate() {
|
||||
if !matches!(block.block_type, EditorBlockType::Mindmap) {
|
||||
continue;
|
||||
}
|
||||
let Some(raw_block) = raw_blocks
|
||||
.iter()
|
||||
.find(|candidate| {
|
||||
read_trimmed_string_field(candidate, &["blockId", "block_id"]).as_deref()
|
||||
== Some(block.block_id.as_str())
|
||||
})
|
||||
.or_else(|| raw_blocks.get(index))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
hydrate_mindmap_block_props_from_raw(block, raw_block);
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate_mindmap_block_props_from_raw(block: &mut EditorBlock, raw_block: &Value) {
|
||||
let props = raw_block.get("props").and_then(Value::as_object);
|
||||
let legacy_data = props
|
||||
.and_then(|map| map.get("data"))
|
||||
.and_then(Value::as_object);
|
||||
if let Some(mindmap_id) = props
|
||||
.and_then(|map| map.get("mindmapId").or_else(|| map.get("mindmap_id")))
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
legacy_data.and_then(|map| {
|
||||
map.get("mindmapId")
|
||||
.or_else(|| map.get("mindmap_id"))
|
||||
.or_else(|| map.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
block
|
||||
.props
|
||||
.extra
|
||||
.insert("mindmapId".into(), Value::String(mindmap_id.to_string()));
|
||||
}
|
||||
for (raw_key, canonical_key) in [("rootNodeId", "rootNodeId"), ("root_node_id", "rootNodeId")] {
|
||||
if let Some(value) = props
|
||||
.and_then(|map| map.get(raw_key))
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| legacy_data.and_then(|map| map.get(raw_key).and_then(Value::as_str)))
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
block
|
||||
.props
|
||||
.extra
|
||||
.insert(canonical_key.into(), Value::String(value.to_string()));
|
||||
}
|
||||
}
|
||||
if let Some(projection_version) = props
|
||||
.and_then(|map| map.get("projectionVersion"))
|
||||
.and_then(Value::as_u64)
|
||||
{
|
||||
block.props.extra.insert(
|
||||
"projectionVersion".into(),
|
||||
Value::Number(projection_version.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_props_from_editor_block(block: &EditorBlock) -> Option<Value> {
|
||||
let mut props = serde_json::Map::new();
|
||||
match block.block_type {
|
||||
@@ -10232,6 +10529,23 @@ fn legacy_props_from_editor_block(block: &EditorBlock) -> Option<Value> {
|
||||
props.insert("tiptapTocNode".into(), tiptap_toc.clone());
|
||||
}
|
||||
}
|
||||
EditorBlockType::Mindmap => {
|
||||
if let Some(mindmap_id) = block.props.extra.get("mindmapId").and_then(Value::as_str) {
|
||||
props.insert("mindmapId".into(), json!(mindmap_id));
|
||||
}
|
||||
if let Some(root_node_id) = block.props.extra.get("rootNodeId").and_then(Value::as_str)
|
||||
{
|
||||
props.insert("rootNodeId".into(), json!(root_node_id));
|
||||
}
|
||||
if let Some(projection_version) = block
|
||||
.props
|
||||
.extra
|
||||
.get("projectionVersion")
|
||||
.and_then(Value::as_u64)
|
||||
{
|
||||
props.insert("projectionVersion".into(), json!(projection_version));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(text_align) = block
|
||||
@@ -10255,6 +10569,7 @@ fn legacy_props_from_editor_block(block: &EditorBlock) -> Option<Value> {
|
||||
fn legacy_type_from_editor_block(block: &EditorBlock) -> &'static str {
|
||||
match block.block_type {
|
||||
EditorBlockType::Paragraph => "paragraph",
|
||||
EditorBlockType::Mindmap => "mindmap",
|
||||
EditorBlockType::Heading => "heading",
|
||||
EditorBlockType::BulletListItem => "bullet_list_item",
|
||||
EditorBlockType::NumberedListItem => "numbered_list_item",
|
||||
@@ -10302,6 +10617,8 @@ fn legacy_content_from_editor_document(document: &EditorBlockDocument) -> Value
|
||||
"props": legacy_props_from_editor_block(block),
|
||||
"content": if matches!(block.block_type, EditorBlockType::Divider) {
|
||||
Value::Array(Vec::new())
|
||||
} else if matches!(block.block_type, EditorBlockType::Mindmap) {
|
||||
Value::String(String::new())
|
||||
} else {
|
||||
Value::String(legacy_text_from_editor_block(block))
|
||||
},
|
||||
@@ -11429,6 +11746,14 @@ mod tests {
|
||||
"text": "新标题"
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json["domainEventPlan"]["eventType"],
|
||||
json!("tree.resource.mindmap.updated")
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json["streamDeltaHint"]["kind"],
|
||||
json!("resync_required")
|
||||
);
|
||||
}
|
||||
RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => {
|
||||
panic!("expected command plan")
|
||||
@@ -11726,16 +12051,44 @@ mod tests {
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "mindmaps:put");
|
||||
assert_eq!(plan.args_json["docId"], json!("doc_1"));
|
||||
assert_eq!(plan.args_json["mindmapId"], json!("mind_1"));
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
plan.args_json["data"],
|
||||
json!({
|
||||
"docId": "doc_1",
|
||||
"mindmapId": "mind_1",
|
||||
"data": {
|
||||
"data": {"text": "中心主题"},
|
||||
"children": [],
|
||||
},
|
||||
"createOnly": true,
|
||||
"data": {"text": "中心主题"},
|
||||
"children": [],
|
||||
})
|
||||
);
|
||||
assert_eq!(plan.args_json["createOnly"], json!(true));
|
||||
assert_eq!(
|
||||
plan.args_json["streamDeltaHint"],
|
||||
json!({
|
||||
"family": "tree",
|
||||
"kind": "resync_required",
|
||||
"args": {
|
||||
"reason": "mindmap.put",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "mind_1",
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json["domainEventPlan"],
|
||||
json!({
|
||||
"family": "tree",
|
||||
"schema": "mnote.tree.domain_event",
|
||||
"schemaVersion": 1,
|
||||
"eventType": "tree.resource.mindmap.put",
|
||||
"streamDeltaHint": {
|
||||
"family": "tree",
|
||||
"kind": "resync_required",
|
||||
"args": {
|
||||
"reason": "mindmap.put",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "mind_1",
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -12317,6 +12670,178 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_save_command_plan_preserves_mindmap_placeholder() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "documents.save".into(),
|
||||
command_id: "cmd_save_mindmap".into(),
|
||||
idempotency_key: Some("idem_save_mindmap".into()),
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("sess_1".into()),
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "rust-web".into(),
|
||||
client: "mnote-web".into(),
|
||||
source_kind: None,
|
||||
root_uri: None,
|
||||
workspace_id: None,
|
||||
capabilities: Vec::new(),
|
||||
},
|
||||
target: Some(RuntimeTargetWire {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("doc_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 6,
|
||||
"content": [],
|
||||
"tiptapDocument": {
|
||||
"type": "doc",
|
||||
"content": [{
|
||||
"type": "paragraph",
|
||||
"attrs": {
|
||||
"blockId": "mind_1",
|
||||
"mnoteBlockType": "mindmap",
|
||||
"mindmapId": "mind_1",
|
||||
"rootNodeId": "root",
|
||||
"projectionVersion": 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
"conflictDetectionKey": "doc_1:6"
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("保存导图占位".into()),
|
||||
refs: vec!["phase6-mindmap".into()],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("documents.save mindmap plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/editorDocument/blocks/0/blockType"),
|
||||
Some(&json!("mindmap"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/type"),
|
||||
Some(&json!("mindmap"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/id"),
|
||||
Some(&json!("mind_1"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/props/mindmapId"),
|
||||
Some(&json!("mind_1"))
|
||||
);
|
||||
assert_eq!(plan.args_json.pointer("/content/0/props/data"), None);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/content"),
|
||||
Some(&json!(""))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_save_command_plan_preserves_mindmap_props_from_editor_document() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "page.body.save".into(),
|
||||
command_id: "cmd_save_mindmap_editor_document".into(),
|
||||
idempotency_key: Some("idem_save_mindmap_editor_document".into()),
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("sess_1".into()),
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "rust-web".into(),
|
||||
client: "mnote-web".into(),
|
||||
source_kind: None,
|
||||
root_uri: None,
|
||||
workspace_id: None,
|
||||
capabilities: Vec::new(),
|
||||
},
|
||||
target: Some(RuntimeTargetWire {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("doc_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 7,
|
||||
"editorDocument": {
|
||||
"documentId": "doc_1",
|
||||
"rootBlockIds": ["block-1"],
|
||||
"blocks": [{
|
||||
"blockId": "block-1",
|
||||
"blockType": "mindmap",
|
||||
"props": {
|
||||
"data": null,
|
||||
"mindmapId": "mind_1",
|
||||
"rootNodeId": "root",
|
||||
"projectionVersion": 1
|
||||
},
|
||||
"contentNodes": [],
|
||||
"childBlockIds": []
|
||||
}]
|
||||
},
|
||||
"content": [],
|
||||
"tiptapDocument": {
|
||||
"type": "doc",
|
||||
"content": [{
|
||||
"type": "paragraph",
|
||||
"attrs": {
|
||||
"blockId": "block-1",
|
||||
"mnoteBlockType": "mindmap",
|
||||
"mindmapId": "mind_1",
|
||||
"rootNodeId": "root",
|
||||
"projectionVersion": 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
"conflictDetectionKey": "doc_1:7"
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("保存 editorDocument 导图占位".into()),
|
||||
refs: vec!["task169-mindmap-realtime-smoke".into()],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("page.body.save mindmap editorDocument plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/props/mindmapId"),
|
||||
Some(&json!("mind_1"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/props/rootNodeId"),
|
||||
Some(&json!("root"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/props/projectionVersion"),
|
||||
Some(&json!(1))
|
||||
);
|
||||
assert_eq!(plan.args_json.pointer("/content/0/props/data"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_save_command_plan_preserves_tiptap_image() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
@@ -16219,6 +16744,7 @@ mod tests {
|
||||
"id": "mind_1",
|
||||
"workspace_id": "ws_1",
|
||||
"document_id": "page_root",
|
||||
"block_id": "block_mind_1",
|
||||
"asset_type": "mindmap",
|
||||
"file_name": "roadmap.json",
|
||||
"mime_type": "application/json",
|
||||
@@ -16293,6 +16819,15 @@ mod tests {
|
||||
item_by_row_id["index:page_root"]["resourceMeta"]["resourceKind"],
|
||||
json!("index")
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["index:page_root"]["resourceMeta"]["objectIdentity"],
|
||||
json!({
|
||||
"objectKind": "index",
|
||||
"documentId": "page_root",
|
||||
"blockId": null,
|
||||
"assetId": null
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["index:page_root"]["resourceMeta"]["extra"]["source"],
|
||||
json!({
|
||||
@@ -16333,6 +16868,24 @@ mod tests {
|
||||
item_by_row_id["asset-folder:mind_1"]["resourceMeta"]["assetKind"],
|
||||
json!("mindmap")
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["asset-folder:mind_1"]["resourceMeta"]["objectIdentity"],
|
||||
json!({
|
||||
"objectKind": "mindmap",
|
||||
"documentId": "page_root",
|
||||
"blockId": "block_mind_1",
|
||||
"assetId": "mind_1"
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["asset-folder:mind_1"]["resourceMeta"]["blockAssetRelation"],
|
||||
json!({
|
||||
"documentId": "page_root",
|
||||
"blockId": "block_mind_1",
|
||||
"assetId": "mind_1",
|
||||
"assetKind": "mindmap"
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["asset:asset_ref_1"]["parentNodeId"],
|
||||
json!("asset-folder:mind_1")
|
||||
@@ -16566,6 +17119,108 @@ mod tests {
|
||||
assert!(!row_ids.contains(&"asset:asset_rust_child"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_file_tree_projection_separates_attachment_object_identities() {
|
||||
let result = execute_runtime_query(RuntimeInput::Query {
|
||||
context: demo_context(),
|
||||
query: RuntimeQueryEnvelopeWire {
|
||||
name: "kernel.project_view".into(),
|
||||
payload: json!({
|
||||
"projection": "file_tree",
|
||||
"workspaceId": "ws_1",
|
||||
"rootNodeId": "page_root",
|
||||
"depth": 2,
|
||||
"includeEdges": true,
|
||||
"nodeTypes": ["page"],
|
||||
}),
|
||||
},
|
||||
data: Some(json!({
|
||||
"documents": [
|
||||
{
|
||||
"id": "page_root",
|
||||
"workspace_id": "ws_1",
|
||||
"title": "根页面",
|
||||
"parent_id": null,
|
||||
"sort_order": 0,
|
||||
"is_starred": true,
|
||||
"is_template": false,
|
||||
"created_at": "2026-04-16T00:00:00Z",
|
||||
"updated_at": "2026-04-16T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"media_assets": [
|
||||
{
|
||||
"id": "office_1",
|
||||
"workspace_id": "ws_1",
|
||||
"document_id": "page_root",
|
||||
"block_id": "block_office_1",
|
||||
"asset_type": "file",
|
||||
"file_name": "contract.docx",
|
||||
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
},
|
||||
{
|
||||
"id": "code_1",
|
||||
"workspace_id": "ws_1",
|
||||
"document_id": "page_root",
|
||||
"block_id": "block_code_1",
|
||||
"asset_type": "file",
|
||||
"file_name": "main.rs",
|
||||
"mime_type": "text/rust"
|
||||
},
|
||||
{
|
||||
"id": "image_1",
|
||||
"workspace_id": "ws_1",
|
||||
"document_id": "page_root",
|
||||
"block_id": "block_image_1",
|
||||
"asset_type": "file",
|
||||
"file_name": "cover.png",
|
||||
"mime_type": "image/png"
|
||||
}
|
||||
],
|
||||
"mindmap_assets": [],
|
||||
"table_assets": [],
|
||||
"mindmap_asset_children": {}
|
||||
})),
|
||||
})
|
||||
.expect("file_tree object identity projection should build");
|
||||
|
||||
let items = result["items"].as_array().expect("items should be array");
|
||||
let item_by_row_id = items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
item.get("rowId")
|
||||
.and_then(Value::as_str)
|
||||
.map(|row_id| (row_id.to_string(), item))
|
||||
})
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
|
||||
assert_eq!(
|
||||
item_by_row_id["asset:office_1"]["resourceMeta"]["resourceKind"],
|
||||
json!("only_office")
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["asset:office_1"]["resourceMeta"]["objectIdentity"]["objectKind"],
|
||||
json!("only_office")
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["asset:code_1"]["resourceMeta"]["resourceKind"],
|
||||
json!("code")
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["asset:code_1"]["resourceMeta"]["objectIdentity"]["objectKind"],
|
||||
json!("code")
|
||||
);
|
||||
assert_eq!(
|
||||
item_by_row_id["asset:image_1"]["resourceMeta"]["objectIdentity"],
|
||||
json!({
|
||||
"objectKind": "attachment",
|
||||
"documentId": "page_root",
|
||||
"blockId": "block_image_1",
|
||||
"assetId": "image_1"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_file_tree_projection_query_matches_index_resource() {
|
||||
let result = execute_runtime_query(RuntimeInput::Query {
|
||||
|
||||
Reference in New Issue
Block a user