feat: 收口 mindmap Phase 6 Leptos UI shell
This commit is contained in:
@@ -51,9 +51,11 @@ pub use kernel::{
|
||||
WorkspaceSourceCapability, WorkspaceSourceKind,
|
||||
};
|
||||
pub use mindmap::{
|
||||
MindmapCommand, MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp,
|
||||
MindmapProjection, MindmapProjectionEdge, MindmapProjectionNode, MindmapProjectionOwner,
|
||||
MindmapTreeNode,
|
||||
MindmapAdapterProjection, MindmapAssociativeLine, MindmapCommand, MindmapKernelCapabilities,
|
||||
MindmapKernelCommand, MindmapKernelEdge, MindmapKernelNode, MindmapKernelProjection,
|
||||
MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp, MindmapProjection,
|
||||
MindmapProjectionEdge, MindmapProjectionNode, MindmapProjectionOwner, MindmapProjectionSource,
|
||||
MindmapSummary, MindmapTreeNode,
|
||||
};
|
||||
pub use page_aggregate::{
|
||||
PageAggregateProjection, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout,
|
||||
@@ -363,4 +365,190 @@ mod tests {
|
||||
assert_eq!(projection.owner, MindmapProjectionOwner::RustKernel);
|
||||
assert!(matches!(command, MindmapCommand::RenameNode { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mindmap_kernel_and_adapter_projection_serialize_as_camel_case() {
|
||||
let kernel_projection = MindmapKernelProjection {
|
||||
schema: "mnote.mindmap.kernel_projection.v1".into(),
|
||||
document_id: "doc_1".into(),
|
||||
mindmap_id: "mind_1".into(),
|
||||
root_node_id: "root".into(),
|
||||
revision: 7,
|
||||
nodes: vec![
|
||||
MindmapKernelNode {
|
||||
node_id: "root".into(),
|
||||
parent_id: None,
|
||||
text: "KMIND".into(),
|
||||
order: 0,
|
||||
collapsed: false,
|
||||
style: Default::default(),
|
||||
refs: Default::default(),
|
||||
},
|
||||
MindmapKernelNode {
|
||||
node_id: "child_1".into(),
|
||||
parent_id: Some("root".into()),
|
||||
text: "二级节点".into(),
|
||||
order: 1,
|
||||
collapsed: false,
|
||||
style: Default::default(),
|
||||
refs: Default::default(),
|
||||
},
|
||||
],
|
||||
edges: vec![MindmapKernelEdge {
|
||||
edge_id: "root->child_1".into(),
|
||||
from_node_id: "root".into(),
|
||||
to_node_id: "child_1".into(),
|
||||
edge_kind: "tree".into(),
|
||||
}],
|
||||
summaries: vec![MindmapSummary {
|
||||
summary_id: "summary_1".into(),
|
||||
parent_node_id: "child_1".into(),
|
||||
node_ids: vec!["child_1".into()],
|
||||
text: "概要".into(),
|
||||
style: Default::default(),
|
||||
}],
|
||||
associative_lines: vec![MindmapAssociativeLine {
|
||||
line_id: "line_1".into(),
|
||||
from_node_id: "root".into(),
|
||||
to_node_id: "child_1".into(),
|
||||
text: Some("关联".into()),
|
||||
style: Default::default(),
|
||||
}],
|
||||
layout: serde_json::json!({ "type": "logicalStructure" }),
|
||||
theme: serde_json::json!({ "template": "classic" }),
|
||||
view: serde_json::json!({ "zoom": 1.0 }),
|
||||
capabilities: MindmapKernelCapabilities {
|
||||
can_edit: true,
|
||||
can_edit_text: true,
|
||||
can_insert_child: true,
|
||||
can_insert_sibling_after: true,
|
||||
can_delete_node: true,
|
||||
can_move_node: true,
|
||||
can_patch_view: true,
|
||||
can_set_layout: true,
|
||||
can_set_theme: true,
|
||||
},
|
||||
source: MindmapProjectionSource::Kernel,
|
||||
};
|
||||
|
||||
let adapter_projection = MindmapAdapterProjection {
|
||||
schema: "mnote.mindmap.simple_mind_map_scene.v1".into(),
|
||||
runtime: "simple-mind-map".into(),
|
||||
root: serde_json::json!({
|
||||
"data": { "uid": "root", "text": "KMIND" },
|
||||
"children": []
|
||||
}),
|
||||
layout: serde_json::json!("logicalStructure"),
|
||||
theme: serde_json::json!("classic"),
|
||||
theme_config: serde_json::json!({}),
|
||||
view: serde_json::json!({ "scale": 1.0 }),
|
||||
config: serde_json::json!({}),
|
||||
compat_payload: serde_json::json!({}),
|
||||
kernel_revision: 7,
|
||||
};
|
||||
|
||||
let kernel_value =
|
||||
serde_json::to_value(kernel_projection).expect("kernel projection should serialize");
|
||||
let adapter_value =
|
||||
serde_json::to_value(adapter_projection).expect("adapter projection should serialize");
|
||||
|
||||
assert_eq!(kernel_value["documentId"], serde_json::json!("doc_1"));
|
||||
assert_eq!(kernel_value["mindmapId"], serde_json::json!("mind_1"));
|
||||
assert_eq!(kernel_value["rootNodeId"], serde_json::json!("root"));
|
||||
assert_eq!(
|
||||
kernel_value["nodes"][1]["parentId"],
|
||||
serde_json::json!("root")
|
||||
);
|
||||
assert_eq!(
|
||||
kernel_value["associativeLines"][0]["fromNodeId"],
|
||||
serde_json::json!("root")
|
||||
);
|
||||
assert_eq!(
|
||||
kernel_value["capabilities"]["canEditText"],
|
||||
serde_json::json!(true)
|
||||
);
|
||||
assert_eq!(kernel_value["source"], serde_json::json!("kernel"));
|
||||
|
||||
assert_eq!(
|
||||
adapter_value["runtime"],
|
||||
serde_json::json!("simple-mind-map")
|
||||
);
|
||||
assert_eq!(adapter_value["themeConfig"], serde_json::json!({}));
|
||||
assert_eq!(adapter_value["compatPayload"], serde_json::json!({}));
|
||||
assert_eq!(adapter_value["kernelRevision"], serde_json::json!(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mindmap_kernel_command_contract_covers_minimum_write_surface() {
|
||||
let commands = vec![
|
||||
MindmapKernelCommand::UpdateText {
|
||||
mindmap_id: "mind_1".into(),
|
||||
node_id: "root".into(),
|
||||
text: "新标题".into(),
|
||||
},
|
||||
MindmapKernelCommand::InsertChild {
|
||||
mindmap_id: "mind_1".into(),
|
||||
parent_node_id: "root".into(),
|
||||
node: MindmapNodeInput {
|
||||
uid: Some("child_1".into()),
|
||||
text: "子节点".into(),
|
||||
hyperlink: None,
|
||||
note: None,
|
||||
refs: None,
|
||||
},
|
||||
},
|
||||
MindmapKernelCommand::InsertSiblingAfter {
|
||||
mindmap_id: "mind_1".into(),
|
||||
target_node_id: "child_1".into(),
|
||||
node: MindmapNodeInput {
|
||||
uid: Some("child_2".into()),
|
||||
text: "同级节点".into(),
|
||||
hyperlink: None,
|
||||
note: None,
|
||||
refs: None,
|
||||
},
|
||||
},
|
||||
MindmapKernelCommand::DeleteNode {
|
||||
mindmap_id: "mind_1".into(),
|
||||
node_id: "child_2".into(),
|
||||
},
|
||||
MindmapKernelCommand::MoveNode {
|
||||
mindmap_id: "mind_1".into(),
|
||||
node_id: "child_1".into(),
|
||||
new_parent_node_id: "root".into(),
|
||||
order: Some(1),
|
||||
},
|
||||
MindmapKernelCommand::PatchView {
|
||||
mindmap_id: "mind_1".into(),
|
||||
patch: serde_json::json!({ "scale": 1.2 }),
|
||||
},
|
||||
MindmapKernelCommand::SetLayout {
|
||||
mindmap_id: "mind_1".into(),
|
||||
layout: serde_json::json!({ "type": "mindMap" }),
|
||||
},
|
||||
MindmapKernelCommand::SetTheme {
|
||||
mindmap_id: "mind_1".into(),
|
||||
theme: serde_json::json!({ "template": "classic" }),
|
||||
theme_config: serde_json::json!({ "lineColor": "#f59e0b" }),
|
||||
},
|
||||
];
|
||||
|
||||
let encoded = serde_json::to_value(&commands).expect("commands should serialize");
|
||||
|
||||
assert_eq!(encoded[0]["type"], serde_json::json!("updateText"));
|
||||
assert_eq!(encoded[1]["type"], serde_json::json!("insertChild"));
|
||||
assert_eq!(encoded[2]["type"], serde_json::json!("insertSiblingAfter"));
|
||||
assert_eq!(encoded[3]["type"], serde_json::json!("deleteNode"));
|
||||
assert_eq!(encoded[4]["type"], serde_json::json!("moveNode"));
|
||||
assert_eq!(encoded[5]["type"], serde_json::json!("patchView"));
|
||||
assert_eq!(encoded[6]["type"], serde_json::json!("setLayout"));
|
||||
assert_eq!(encoded[7]["type"], serde_json::json!("setTheme"));
|
||||
assert_eq!(encoded[0]["mindmapId"], serde_json::json!("mind_1"));
|
||||
assert_eq!(encoded[1]["parentNodeId"], serde_json::json!("root"));
|
||||
assert_eq!(encoded[4]["newParentNodeId"], serde_json::json!("root"));
|
||||
assert_eq!(
|
||||
encoded[7]["themeConfig"]["lineColor"],
|
||||
serde_json::json!("#f59e0b")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,189 @@ pub enum MindmapProjectionOwner {
|
||||
CompatBlob,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum MindmapProjectionSource {
|
||||
Kernel,
|
||||
CompatBlob,
|
||||
AdapterCache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapKernelProjection {
|
||||
pub schema: String,
|
||||
pub document_id: String,
|
||||
pub mindmap_id: String,
|
||||
pub root_node_id: String,
|
||||
pub revision: u64,
|
||||
#[serde(default)]
|
||||
pub nodes: Vec<MindmapKernelNode>,
|
||||
#[serde(default)]
|
||||
pub edges: Vec<MindmapKernelEdge>,
|
||||
#[serde(default)]
|
||||
pub summaries: Vec<MindmapSummary>,
|
||||
#[serde(default)]
|
||||
pub associative_lines: Vec<MindmapAssociativeLine>,
|
||||
#[serde(default)]
|
||||
pub layout: Value,
|
||||
#[serde(default)]
|
||||
pub theme: Value,
|
||||
#[serde(default)]
|
||||
pub view: Value,
|
||||
pub capabilities: MindmapKernelCapabilities,
|
||||
pub source: MindmapProjectionSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapKernelNode {
|
||||
pub node_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<String>,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub order: i64,
|
||||
#[serde(default)]
|
||||
pub collapsed: bool,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub style: BTreeMap<String, Value>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub refs: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapKernelEdge {
|
||||
pub edge_id: String,
|
||||
pub from_node_id: String,
|
||||
pub to_node_id: String,
|
||||
pub edge_kind: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapSummary {
|
||||
pub summary_id: String,
|
||||
pub parent_node_id: String,
|
||||
#[serde(default)]
|
||||
pub node_ids: Vec<String>,
|
||||
pub text: String,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub style: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapAssociativeLine {
|
||||
pub line_id: String,
|
||||
pub from_node_id: String,
|
||||
pub to_node_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub style: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapKernelCapabilities {
|
||||
pub can_edit: bool,
|
||||
pub can_edit_text: bool,
|
||||
pub can_insert_child: bool,
|
||||
pub can_insert_sibling_after: bool,
|
||||
pub can_delete_node: bool,
|
||||
pub can_move_node: bool,
|
||||
pub can_patch_view: bool,
|
||||
pub can_set_layout: bool,
|
||||
pub can_set_theme: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapAdapterProjection {
|
||||
pub schema: String,
|
||||
pub runtime: String,
|
||||
pub root: Value,
|
||||
#[serde(default)]
|
||||
pub layout: Value,
|
||||
#[serde(default)]
|
||||
pub theme: Value,
|
||||
#[serde(default)]
|
||||
pub theme_config: Value,
|
||||
#[serde(default)]
|
||||
pub view: Value,
|
||||
#[serde(default)]
|
||||
pub config: Value,
|
||||
#[serde(default)]
|
||||
pub compat_payload: Value,
|
||||
pub kernel_revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum MindmapKernelCommand {
|
||||
UpdateText {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
#[serde(rename = "nodeId", alias = "node_id")]
|
||||
node_id: String,
|
||||
text: String,
|
||||
},
|
||||
InsertChild {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
#[serde(rename = "parentNodeId", alias = "parent_node_id", alias = "parentId")]
|
||||
parent_node_id: String,
|
||||
node: MindmapNodeInput,
|
||||
},
|
||||
InsertSiblingAfter {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
#[serde(rename = "targetNodeId", alias = "target_node_id")]
|
||||
target_node_id: String,
|
||||
node: MindmapNodeInput,
|
||||
},
|
||||
DeleteNode {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
#[serde(rename = "nodeId", alias = "node_id")]
|
||||
node_id: String,
|
||||
},
|
||||
MoveNode {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
#[serde(rename = "nodeId", alias = "node_id")]
|
||||
node_id: String,
|
||||
#[serde(
|
||||
rename = "newParentNodeId",
|
||||
alias = "new_parent_node_id",
|
||||
alias = "newParentId"
|
||||
)]
|
||||
new_parent_node_id: String,
|
||||
#[serde(rename = "order", alias = "sortOrder", alias = "sort_order")]
|
||||
order: Option<i64>,
|
||||
},
|
||||
PatchView {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
patch: Value,
|
||||
},
|
||||
SetLayout {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
layout: Value,
|
||||
},
|
||||
SetTheme {
|
||||
#[serde(rename = "mindmapId", alias = "mindmap_id", alias = "mapId")]
|
||||
mindmap_id: String,
|
||||
theme: Value,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "themeConfig", alias = "theme_config")]
|
||||
theme_config: Value,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum MindmapCommand {
|
||||
@@ -136,6 +319,8 @@ pub struct MindmapTreeNode {
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
/// 兼容 DTO:对应 simple-mind-map / KMind 风格树节点。
|
||||
/// 长期事实源应使用 `MindmapKernelProjection` 与 kernel command。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapNodeInput {
|
||||
@@ -150,6 +335,8 @@ pub struct MindmapNodeInput {
|
||||
pub refs: Option<Vec<MindmapNodeRef>>,
|
||||
}
|
||||
|
||||
/// 兼容 DTO:用于历史 AI 工具和旧 blob 操作。
|
||||
/// 新的 leptos-mindmap 写面应优先使用 `MindmapKernelCommand`。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "op", rename_all = "camelCase")]
|
||||
pub enum MindmapOp {
|
||||
|
||||
Reference in New Issue
Block a user