20260513 mindmap优化01

This commit is contained in:
lix-2026
2026-05-13 22:43:16 +08:00
parent 17c003976b
commit b4a452a8b7
89 changed files with 11557 additions and 707 deletions
@@ -54,6 +54,10 @@ mod tests {
serde_json::to_string(&EditorBlockType::PageReference).unwrap(),
"\"page_reference\""
);
assert_eq!(
serde_json::to_string(&EditorBlockType::Mindmap).unwrap(),
"\"mindmap\""
);
assert_eq!(
serde_json::to_string(&EditorBlockType::BlockReference).unwrap(),
"\"block_reference\""
@@ -6,6 +6,7 @@ use std::collections::BTreeMap;
#[serde(rename_all = "snake_case")]
pub enum EditorBlockType {
Paragraph,
Mindmap,
Heading,
BulletListItem,
NumberedListItem,
@@ -58,6 +58,16 @@ pub struct TiptapParagraphAttrs {
pub block_id: Option<String>,
#[serde(default)]
pub text_align: Option<String>,
#[serde(default)]
pub mnote_block_type: Option<String>,
#[serde(default)]
pub mindmap_id: Option<String>,
#[serde(default)]
pub root_node_id: Option<String>,
#[serde(default)]
pub projection_version: Option<u64>,
#[serde(flatten, default)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
@@ -371,6 +381,54 @@ fn props_with_text_align(text_align: Option<String>) -> BlockProps {
props
}
fn props_with_mindmap_attrs(attrs: &TiptapParagraphAttrs) -> BlockProps {
let mut props = props_with_text_align(attrs.text_align.clone());
if let Some(mindmap_id) = attrs
.mindmap_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
props
.extra
.insert("mindmapId".into(), Value::String(mindmap_id.to_string()));
}
if let Some(root_node_id) = attrs
.root_node_id
.as_deref()
.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) = attrs.projection_version {
props.extra.insert(
"projectionVersion".into(),
Value::Number(projection_version.into()),
);
}
for (key, value) in &attrs.extra {
props.extra.insert(key.clone(), value.clone());
}
props
}
fn read_extra_string(props: &BlockProps, key: &str) -> Option<String> {
props
.extra
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn read_extra_u64(props: &BlockProps, key: &str) -> Option<u64> {
props.extra.get(key).and_then(Value::as_u64)
}
impl EditorBlockDocumentTiptapBridge {
pub fn to_tiptap_doc(
document: &EditorBlockDocument,
@@ -474,6 +532,11 @@ fn list_item_content_from_block(
attrs: TiptapParagraphAttrs {
block_id: Some(block.block_id.clone()),
text_align: text_align_from_props(&block.props),
mnote_block_type: None,
mindmap_id: None,
root_node_id: None,
projection_version: None,
extra: BTreeMap::new(),
},
content: inline_content,
}];
@@ -491,9 +554,40 @@ fn block_to_tiptap_node(
attrs: TiptapParagraphAttrs {
block_id: Some(block.block_id.clone()),
text_align: text_align_from_props(&block.props),
mnote_block_type: None,
mindmap_id: None,
root_node_id: None,
projection_version: None,
extra: BTreeMap::new(),
},
content,
}),
EditorBlockType::Mindmap => {
let mut extra = block.props.extra.clone();
extra.remove("textAlign");
extra.remove("text_align");
extra.remove("mindmapId");
extra.remove("rootNodeId");
extra.remove("projectionVersion");
if !extra.contains_key("mnoteMindmapData") {
if let Some(data) = block.props.extra.get("data") {
extra.insert("mnoteMindmapData".into(), data.clone());
}
}
Ok(TiptapNode::Paragraph {
attrs: TiptapParagraphAttrs {
block_id: Some(block.block_id.clone()),
text_align: text_align_from_props(&block.props),
mnote_block_type: Some("mindmap".into()),
mindmap_id: read_extra_string(&block.props, "mindmapId")
.or_else(|| Some(block.block_id.clone())),
root_node_id: read_extra_string(&block.props, "rootNodeId"),
projection_version: read_extra_u64(&block.props, "projectionVersion"),
extra,
},
content: Vec::new(),
})
}
EditorBlockType::Heading => Ok(TiptapNode::Heading {
attrs: TiptapHeadingAttrs {
level: block.props.heading_level.unwrap_or(1),
@@ -546,6 +640,11 @@ fn block_to_tiptap_node(
attrs: TiptapParagraphAttrs {
block_id: Some(block.block_id.clone()),
text_align: text_align_from_props(&block.props),
mnote_block_type: None,
mindmap_id: None,
root_node_id: None,
projection_version: None,
extra: BTreeMap::new(),
},
content,
}],
@@ -584,6 +683,21 @@ fn node_to_block(
) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {
let fallback_block_id = format!("block_{}", index + 1);
match node {
TiptapNode::Paragraph { attrs, .. }
if attrs.mnote_block_type.as_deref() == Some("mindmap") =>
{
Ok(EditorBlock {
block_id: attrs
.block_id
.clone()
.or_else(|| attrs.mindmap_id.clone())
.unwrap_or(fallback_block_id),
block_type: EditorBlockType::Mindmap,
props: props_with_mindmap_attrs(attrs),
content_nodes: vec![],
child_block_ids: vec![],
})
}
TiptapNode::Paragraph { attrs, content } => Ok(EditorBlock {
block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
block_type: EditorBlockType::Paragraph,
@@ -1118,6 +1232,7 @@ mod tests {
attrs: TiptapParagraphAttrs {
block_id: Some("align_1".into()),
text_align: Some("center".into()),
..TiptapParagraphAttrs::default()
},
content: vec![TiptapNode::Text {
text: "E20 center".into(),
@@ -1143,4 +1258,34 @@ mod tests {
};
assert_eq!(attrs.text_align.as_deref(), Some("center"));
}
#[test]
fn preserves_mindmap_paragraph_placeholder() {
let doc = TiptapNode::Doc {
content: vec![TiptapNode::Paragraph {
attrs: TiptapParagraphAttrs {
block_id: Some("mind_1".into()),
mnote_block_type: Some("mindmap".into()),
mindmap_id: Some("mind_1".into()),
root_node_id: Some("root".into()),
projection_version: Some(1),
..TiptapParagraphAttrs::default()
},
content: vec![],
}],
};
let parsed = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_1", &doc)
.expect("mindmap placeholder should convert to mindmap block");
assert_eq!(parsed.root_block_ids, vec!["mind_1"]);
assert_eq!(parsed.blocks[0].block_type, EditorBlockType::Mindmap);
assert_eq!(
parsed.blocks[0].props.extra.get("mindmapId"),
Some(&serde_json::json!("mind_1"))
);
let restored = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&parsed)
.expect("mindmap block should restore to paragraph placeholder");
assert_eq!(restored, doc);
}
}
+38
View File
@@ -112,6 +112,9 @@ pub enum KernelProjectionResourceKind {
Asset,
AssetFolder,
Mindmap,
Attachment,
OnlyOffice,
Code,
Table,
Book,
Pdf,
@@ -131,6 +134,35 @@ pub enum KernelProjectionAssetKind {
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelObjectKind {
Page,
Index,
Mindmap,
Attachment,
OnlyOffice,
Code,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelObjectIdentity {
pub object_kind: KernelObjectKind,
pub document_id: Option<String>,
pub block_id: Option<String>,
pub asset_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct KernelBlockAssetRelation {
pub document_id: String,
pub block_id: String,
pub asset_id: String,
pub asset_kind: KernelProjectionAssetKind,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KernelGraphDirection {
@@ -367,6 +399,10 @@ pub struct KernelProjectionResourceMeta {
pub workspace_id: Option<String>,
pub asset_kind: Option<KernelProjectionAssetKind>,
pub icon_hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub object_identity: Option<KernelObjectIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_asset_relation: Option<KernelBlockAssetRelation>,
#[serde(default)]
pub extra: BTreeMap<String, Value>,
}
@@ -649,6 +685,8 @@ mod tests {
workspace_id: Some("ws_1".into()),
asset_kind: Some(KernelProjectionAssetKind::File),
icon_hint: Some("page".into()),
object_identity: None,
block_asset_relation: None,
extra: BTreeMap::new(),
}),
icon_hint: Some("page".into()),
+5 -4
View File
@@ -40,10 +40,11 @@ pub use kernel::{
DocumentContentResult, DocumentReadEvidenceItem, DocumentReadEvidenceKind, DocumentReadNode,
DocumentReadNodeMeta, DocumentReadNodeType, DocumentReadOutlineEntry, DocumentReadPageSubtree,
DocumentReadStats, DocumentReadSubtree, KernelAttachEdge, KernelAuditStamp,
KernelContentPayload, KernelCreateNode, KernelDetachEdge, KernelEdge, KernelEdgeListResult,
KernelEdgeType, KernelGetNode, KernelGetSubtree, KernelGraphDirection,
KernelGraphTraversalResult, KernelGraphVisit, KernelListChildren, KernelListEdges,
KernelMoveSubtree, KernelNode, KernelNodeMetadata, KernelNodeType, KernelProjectionAssetKind,
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,