20260513 mindmap优化01
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -150,6 +150,7 @@ fn tiptap_import_preserves_explicit_block_ids_and_marks() {
|
||||
attrs: TiptapParagraphAttrs {
|
||||
block_id: Some("p-1".into()),
|
||||
text_align: None,
|
||||
..TiptapParagraphAttrs::default()
|
||||
},
|
||||
content: vec![TiptapNode::Text {
|
||||
text: "Hello".into(),
|
||||
@@ -183,6 +184,7 @@ fn tiptap_import_preserves_explicit_block_ids_and_marks() {
|
||||
attrs: TiptapParagraphAttrs {
|
||||
block_id: Some("t-1".into()),
|
||||
text_align: None,
|
||||
..TiptapParagraphAttrs::default()
|
||||
},
|
||||
content: vec![TiptapNode::Text {
|
||||
text: "todo".into(),
|
||||
@@ -335,3 +337,41 @@ fn tiptap_table_round_trip_preserves_table_node() {
|
||||
Some(&serde_json::json!("A1"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiptap_mindmap_placeholder_round_trip_preserves_mindmap_attrs() {
|
||||
let doc: TiptapNode = serde_json::from_value(serde_json::json!({
|
||||
"type": "doc",
|
||||
"content": [{
|
||||
"type": "paragraph",
|
||||
"attrs": {
|
||||
"blockId": "mind_1",
|
||||
"mnoteBlockType": "mindmap",
|
||||
"mindmapId": "mind_1",
|
||||
"rootNodeId": "root",
|
||||
"projectionVersion": 1
|
||||
}
|
||||
}]
|
||||
}))
|
||||
.expect("mindmap placeholder JSON should parse");
|
||||
|
||||
let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_mindmap", &doc)
|
||||
.expect("mindmap placeholder should import");
|
||||
assert_eq!(imported.blocks[0].block_type, EditorBlockType::Mindmap);
|
||||
assert_eq!(
|
||||
imported.blocks[0].props.extra.get("mindmapId"),
|
||||
Some(&serde_json::json!("mind_1"))
|
||||
);
|
||||
|
||||
let exported =
|
||||
EditorBlockDocumentTiptapBridge::to_tiptap_doc(&imported).expect("mindmap should export");
|
||||
let exported_value = serde_json::to_value(exported).expect("exported doc should serialize");
|
||||
assert_eq!(
|
||||
exported_value.pointer("/content/0/attrs/mnoteBlockType"),
|
||||
Some(&serde_json::json!("mindmap"))
|
||||
);
|
||||
assert_eq!(
|
||||
exported_value.pointer("/content/0/attrs/mindmapId"),
|
||||
Some(&serde_json::json!("mind_1"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use core_protocol::{
|
||||
KernelBlockAssetRelation, KernelObjectIdentity, KernelObjectKind, KernelProjectionAssetKind,
|
||||
KernelProjectionResourceKind, KernelProjectionResourceMeta,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn object_identity_separates_index_body_from_mindmap_asset() {
|
||||
let index_identity = KernelObjectIdentity {
|
||||
object_kind: KernelObjectKind::Index,
|
||||
document_id: Some("doc_1".into()),
|
||||
block_id: None,
|
||||
asset_id: None,
|
||||
};
|
||||
let mindmap_identity = KernelObjectIdentity {
|
||||
object_kind: KernelObjectKind::Mindmap,
|
||||
document_id: Some("doc_1".into()),
|
||||
block_id: Some("block_mindmap_1".into()),
|
||||
asset_id: Some("mind_1".into()),
|
||||
};
|
||||
|
||||
assert_ne!(index_identity, mindmap_identity);
|
||||
|
||||
let index_json = serde_json::to_value(&index_identity).expect("index identity 序列化");
|
||||
let mindmap_json = serde_json::to_value(&mindmap_identity).expect("mindmap identity 序列化");
|
||||
|
||||
assert_eq!(index_json["objectKind"], "index");
|
||||
assert_eq!(index_json["documentId"], "doc_1");
|
||||
assert_eq!(mindmap_json["objectKind"], "mindmap");
|
||||
assert_eq!(mindmap_json["assetId"], "mind_1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_meta_can_describe_asset_object_kinds() {
|
||||
let cases = [
|
||||
(
|
||||
KernelObjectKind::Mindmap,
|
||||
KernelProjectionResourceKind::Mindmap,
|
||||
KernelProjectionAssetKind::Mindmap,
|
||||
"mind_1",
|
||||
),
|
||||
(
|
||||
KernelObjectKind::OnlyOffice,
|
||||
KernelProjectionResourceKind::OnlyOffice,
|
||||
KernelProjectionAssetKind::File,
|
||||
"office_1",
|
||||
),
|
||||
(
|
||||
KernelObjectKind::Attachment,
|
||||
KernelProjectionResourceKind::Attachment,
|
||||
KernelProjectionAssetKind::Image,
|
||||
"image_1",
|
||||
),
|
||||
(
|
||||
KernelObjectKind::Code,
|
||||
KernelProjectionResourceKind::Code,
|
||||
KernelProjectionAssetKind::File,
|
||||
"code_1",
|
||||
),
|
||||
];
|
||||
|
||||
for (object_kind, resource_kind, asset_kind, asset_id) in cases {
|
||||
let meta = KernelProjectionResourceMeta {
|
||||
resource_kind: Some(resource_kind),
|
||||
document_id: Some("doc_1".into()),
|
||||
asset_id: Some(asset_id.into()),
|
||||
workspace_id: Some("ws_1".into()),
|
||||
asset_kind: Some(asset_kind.clone()),
|
||||
icon_hint: None,
|
||||
object_identity: Some(KernelObjectIdentity {
|
||||
object_kind,
|
||||
document_id: Some("doc_1".into()),
|
||||
block_id: Some("block_1".into()),
|
||||
asset_id: Some(asset_id.into()),
|
||||
}),
|
||||
block_asset_relation: Some(KernelBlockAssetRelation {
|
||||
document_id: "doc_1".into(),
|
||||
block_id: "block_1".into(),
|
||||
asset_id: asset_id.into(),
|
||||
asset_kind,
|
||||
}),
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&meta).expect("resource meta 序列化");
|
||||
assert_eq!(json["objectIdentity"]["documentId"], "doc_1");
|
||||
assert_eq!(json["blockAssetRelation"]["blockId"], "block_1");
|
||||
assert_eq!(json["blockAssetRelation"]["assetId"], asset_id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user