diff --git a/collab/src/document/blocks/mention_helper.rs b/collab/src/document/blocks/mention_helper.rs new file mode 100644 index 000000000..e9802e249 --- /dev/null +++ b/collab/src/document/blocks/mention_helper.rs @@ -0,0 +1,520 @@ +/// Mention block helper functions that mirror Flutter's MentionBlockKeys utilities +/// +/// This module provides helper functions to create mention block deltas following +/// the same structure as Flutter's `buildMentionXXXAttributes` functions. +/// +/// All mention blocks use the special character '$' as the inserted text, with +/// attributes containing the mention metadata. +use super::text_entities::TextDelta; +use crate::preclude::{Any, Attrs}; +use std::collections::HashMap; + +/// The special character used for all mention blocks +pub const MENTION_CHAR: &str = "$"; + +/// Attribute keys used in mention blocks +pub mod mention_keys { + pub const MENTION: &str = "mention"; + pub const TYPE: &str = "type"; + pub const PAGE_ID: &str = "page_id"; + pub const BLOCK_ID: &str = "block_id"; + pub const ROW_ID: &str = "row_id"; + pub const URL: &str = "url"; + pub const DATE: &str = "date"; + pub const INCLUDE_TIME: &str = "include_time"; + pub const REMINDER_ID: &str = "reminder_id"; + pub const REMINDER_OPTION: &str = "reminder_option"; + pub const PERSON_ID: &str = "person_id"; + pub const PERSON_NAME: &str = "person_name"; +} + +/// Mention type constants +pub mod mention_types { + pub const PERSON: &str = "person"; + pub const PAGE: &str = "page"; + pub const CHILD_PAGE: &str = "childPage"; + pub const DATE: &str = "date"; + pub const REMINDER: &str = "reminder"; // Backward compatibility alias for 'date' + pub const EXTERNAL_LINK: &str = "externalLink"; +} + +/// Builder for person mention attributes +/// +/// # Example +/// ``` +/// use collab::document::blocks::*; +/// +/// let delta = build_mention_person_delta( +/// "user123", +/// "John Doe", +/// "doc456", +/// Some("block789"), +/// None, +/// ); +/// ``` +pub fn build_mention_person_delta( + person_id: &str, + person_name: &str, + page_id: &str, + block_id: Option<&str>, + row_id: Option<&str>, +) -> TextDelta { + let mut mention_content = HashMap::new(); + mention_content.insert( + mention_keys::TYPE.to_string(), + mention_types::PERSON.to_string(), + ); + mention_content.insert(mention_keys::PERSON_ID.to_string(), person_id.to_string()); + mention_content.insert( + mention_keys::PERSON_NAME.to_string(), + person_name.to_string(), + ); + mention_content.insert(mention_keys::PAGE_ID.to_string(), page_id.to_string()); + + if let Some(block_id) = block_id { + mention_content.insert(mention_keys::BLOCK_ID.to_string(), block_id.to_string()); + } + if let Some(row_id) = row_id { + mention_content.insert(mention_keys::ROW_ID.to_string(), row_id.to_string()); + } + + let mut attrs = Attrs::new(); + attrs.insert(mention_keys::MENTION.into(), Any::from(mention_content)); + + TextDelta::Inserted(MENTION_CHAR.to_string(), Some(attrs)) +} + +/// Builder for page/childPage mention attributes +/// +/// # Example +/// ``` +/// use collab::document::blocks::*; +/// +/// // Regular page mention +/// let page_delta = build_mention_page_delta( +/// MentionPageType::Page, +/// "page123", +/// Some("block456"), +/// None, +/// ); +/// +/// // Child page mention +/// let child_delta = build_mention_page_delta( +/// MentionPageType::ChildPage, +/// "page789", +/// None, +/// None, +/// ); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MentionPageType { + Page, + ChildPage, +} + +impl MentionPageType { + pub fn as_str(&self) -> &'static str { + match self { + Self::Page => mention_types::PAGE, + Self::ChildPage => mention_types::CHILD_PAGE, + } + } +} + +pub fn build_mention_page_delta( + mention_type: MentionPageType, + page_id: &str, + block_id: Option<&str>, + row_id: Option<&str>, +) -> TextDelta { + let mut mention_content = HashMap::new(); + mention_content.insert( + mention_keys::TYPE.to_string(), + mention_type.as_str().to_string(), + ); + mention_content.insert(mention_keys::PAGE_ID.to_string(), page_id.to_string()); + + if let Some(block_id) = block_id { + mention_content.insert(mention_keys::BLOCK_ID.to_string(), block_id.to_string()); + } + if let Some(row_id) = row_id { + mention_content.insert(mention_keys::ROW_ID.to_string(), row_id.to_string()); + } + + let mut attrs = Attrs::new(); + attrs.insert(mention_keys::MENTION.into(), Any::from(mention_content)); + + TextDelta::Inserted(MENTION_CHAR.to_string(), Some(attrs)) +} + +/// Builder for date/reminder mention attributes +/// +/// # Example +/// ``` +/// use collab::document::blocks::*; +/// +/// let delta = build_mention_date_delta( +/// "2025-01-30T10:00:00Z", +/// Some("reminder123"), +/// Some("atTimeOfEvent"), +/// true, // include_time +/// ); +/// ``` +pub fn build_mention_date_delta( + date: &str, + reminder_id: Option<&str>, + reminder_option: Option<&str>, + include_time: bool, +) -> TextDelta { + let mut mention_content = HashMap::new(); + mention_content.insert( + mention_keys::TYPE.to_string(), + mention_types::DATE.to_string(), + ); + mention_content.insert(mention_keys::DATE.to_string(), date.to_string()); + mention_content.insert( + mention_keys::INCLUDE_TIME.to_string(), + include_time.to_string(), + ); + + if let Some(reminder_id) = reminder_id { + mention_content.insert( + mention_keys::REMINDER_ID.to_string(), + reminder_id.to_string(), + ); + } + if let Some(reminder_option) = reminder_option { + mention_content.insert( + mention_keys::REMINDER_OPTION.to_string(), + reminder_option.to_string(), + ); + } + + let mut attrs = Attrs::new(); + attrs.insert(mention_keys::MENTION.into(), Any::from(mention_content)); + + TextDelta::Inserted(MENTION_CHAR.to_string(), Some(attrs)) +} + +/// Builder for external link mention attributes +/// +/// # Example +/// ``` +/// use collab::document::blocks::*; +/// +/// let delta = build_mention_external_link_delta("https://example.com"); +/// ``` +pub fn build_mention_external_link_delta(url: &str) -> TextDelta { + let mut mention_content = HashMap::new(); + mention_content.insert( + mention_keys::TYPE.to_string(), + mention_types::EXTERNAL_LINK.to_string(), + ); + mention_content.insert(mention_keys::URL.to_string(), url.to_string()); + + let mut attrs = Attrs::new(); + attrs.insert(mention_keys::MENTION.into(), Any::from(mention_content)); + + TextDelta::Inserted(MENTION_CHAR.to_string(), Some(attrs)) +} + +/// Extract mention type from a TextDelta +/// +/// Returns None if the delta is not a mention or doesn't have a type field +pub fn extract_mention_type(delta: &TextDelta) -> Option { + match delta { + TextDelta::Inserted(text, Some(attrs)) if text == MENTION_CHAR => { + if let Some(Any::Map(mention_map)) = attrs.get(mention_keys::MENTION) { + mention_map.get(mention_keys::TYPE).map(|v| v.to_string()) + } else { + None + } + }, + _ => None, + } +} + +/// Extract person ID from a person mention delta +pub fn extract_person_id(delta: &TextDelta) -> Option { + match delta { + TextDelta::Inserted(text, Some(attrs)) if text == MENTION_CHAR => { + if let Some(Any::Map(mention_map)) = attrs.get(mention_keys::MENTION) { + mention_map + .get(mention_keys::PERSON_ID) + .map(|v| v.to_string()) + } else { + None + } + }, + _ => None, + } +} + +/// Extract page ID from a page/childPage mention delta +pub fn extract_page_id(delta: &TextDelta) -> Option { + match delta { + TextDelta::Inserted(text, Some(attrs)) if text == MENTION_CHAR => { + if let Some(Any::Map(mention_map)) = attrs.get(mention_keys::MENTION) { + mention_map + .get(mention_keys::PAGE_ID) + .map(|v| v.to_string()) + } else { + None + } + }, + _ => None, + } +} + +/// Extract date from a date/reminder mention delta +pub fn extract_date(delta: &TextDelta) -> Option { + match delta { + TextDelta::Inserted(text, Some(attrs)) if text == MENTION_CHAR => { + if let Some(Any::Map(mention_map)) = attrs.get(mention_keys::MENTION) { + mention_map.get(mention_keys::DATE).map(|v| v.to_string()) + } else { + None + } + }, + _ => None, + } +} + +/// Extract URL from an external link mention delta +pub fn extract_url(delta: &TextDelta) -> Option { + match delta { + TextDelta::Inserted(text, Some(attrs)) if text == MENTION_CHAR => { + if let Some(Any::Map(mention_map)) = attrs.get(mention_keys::MENTION) { + mention_map.get(mention_keys::URL).map(|v| v.to_string()) + } else { + None + } + }, + _ => None, + } +} + +/// Check if a delta is a mention block +pub fn is_mention(delta: &TextDelta) -> bool { + match delta { + TextDelta::Inserted(text, Some(attrs)) if text == MENTION_CHAR => { + attrs.contains_key(mention_keys::MENTION) + }, + _ => false, + } +} + +/// Comprehensive mention data extractor +#[derive(Debug, Clone, PartialEq)] +pub enum MentionData { + Person { + person_id: String, + person_name: String, + page_id: String, + block_id: Option, + row_id: Option, + }, + Page { + page_id: String, + block_id: Option, + row_id: Option, + }, + ChildPage { + page_id: String, + }, + Date { + date: String, + include_time: bool, + reminder_id: Option, + reminder_option: Option, + }, + ExternalLink { + url: String, + }, +} + +/// Extract all mention data from a TextDelta +/// +/// Returns Some(MentionData) if the delta is a valid mention, None otherwise +pub fn extract_mention_data(delta: &TextDelta) -> Option { + match delta { + TextDelta::Inserted(text, Some(attrs)) if text == MENTION_CHAR => { + if let Some(Any::Map(mention_map)) = attrs.get(mention_keys::MENTION) { + let mention_type = mention_map.get(mention_keys::TYPE)?.to_string(); + + match mention_type.as_str() { + mention_types::PERSON => Some(MentionData::Person { + person_id: mention_map.get(mention_keys::PERSON_ID)?.to_string(), + person_name: mention_map.get(mention_keys::PERSON_NAME)?.to_string(), + page_id: mention_map.get(mention_keys::PAGE_ID)?.to_string(), + block_id: mention_map + .get(mention_keys::BLOCK_ID) + .map(|v| v.to_string()), + row_id: mention_map.get(mention_keys::ROW_ID).map(|v| v.to_string()), + }), + mention_types::PAGE => Some(MentionData::Page { + page_id: mention_map.get(mention_keys::PAGE_ID)?.to_string(), + block_id: mention_map + .get(mention_keys::BLOCK_ID) + .map(|v| v.to_string()), + row_id: mention_map.get(mention_keys::ROW_ID).map(|v| v.to_string()), + }), + mention_types::CHILD_PAGE => Some(MentionData::ChildPage { + page_id: mention_map.get(mention_keys::PAGE_ID)?.to_string(), + }), + mention_types::DATE | mention_types::REMINDER => { + let date = mention_map.get(mention_keys::DATE)?.to_string(); + let include_time = mention_map + .get(mention_keys::INCLUDE_TIME) + .and_then(|v| v.to_string().parse::().ok()) + .unwrap_or(false); + Some(MentionData::Date { + date, + include_time, + reminder_id: mention_map + .get(mention_keys::REMINDER_ID) + .map(|v| v.to_string()), + reminder_option: mention_map + .get(mention_keys::REMINDER_OPTION) + .map(|v| v.to_string()), + }) + }, + mention_types::EXTERNAL_LINK => Some(MentionData::ExternalLink { + url: mention_map.get(mention_keys::URL)?.to_string(), + }), + _ => None, + } + } else { + None + } + }, + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_person_mention() { + let delta = build_mention_person_delta("user123", "John Doe", "doc456", Some("block789"), None); + + assert!(is_mention(&delta)); + assert_eq!(extract_mention_type(&delta), Some("person".to_string())); + assert_eq!(extract_person_id(&delta), Some("user123".to_string())); + + if let Some(MentionData::Person { + person_id, + person_name, + page_id, + block_id, + row_id, + }) = extract_mention_data(&delta) + { + assert_eq!(person_id, "user123"); + assert_eq!(person_name, "John Doe"); + assert_eq!(page_id, "doc456"); + assert_eq!(block_id, Some("block789".to_string())); + assert_eq!(row_id, None); + } else { + panic!("Expected Person mention data"); + } + } + + #[test] + fn test_page_mention() { + let delta = build_mention_page_delta(MentionPageType::Page, "page123", Some("block456"), None); + + assert!(is_mention(&delta)); + assert_eq!(extract_mention_type(&delta), Some("page".to_string())); + assert_eq!(extract_page_id(&delta), Some("page123".to_string())); + + if let Some(MentionData::Page { + page_id, + block_id, + row_id, + }) = extract_mention_data(&delta) + { + assert_eq!(page_id, "page123"); + assert_eq!(block_id, Some("block456".to_string())); + assert_eq!(row_id, None); + } else { + panic!("Expected Page mention data"); + } + } + + #[test] + fn test_child_page_mention() { + let delta = build_mention_page_delta(MentionPageType::ChildPage, "page789", None, None); + + assert!(is_mention(&delta)); + assert_eq!(extract_mention_type(&delta), Some("childPage".to_string())); + assert_eq!(extract_page_id(&delta), Some("page789".to_string())); + + if let Some(MentionData::ChildPage { page_id }) = extract_mention_data(&delta) { + assert_eq!(page_id, "page789"); + } else { + panic!("Expected ChildPage mention data"); + } + } + + #[test] + fn test_date_mention() { + let delta = build_mention_date_delta( + "2025-01-30T12:00:00.000Z", + Some("reminder123"), + Some("atTimeOfEvent"), + true, + ); + + assert!(is_mention(&delta)); + assert_eq!(extract_mention_type(&delta), Some("date".to_string())); + assert_eq!( + extract_date(&delta), + Some("2025-01-30T12:00:00.000Z".to_string()) + ); + + if let Some(MentionData::Date { + date, + include_time, + reminder_id, + reminder_option, + }) = extract_mention_data(&delta) + { + assert_eq!(date, "2025-01-30T12:00:00.000Z"); + assert!(include_time); + assert_eq!(reminder_id, Some("reminder123".to_string())); + assert_eq!(reminder_option, Some("atTimeOfEvent".to_string())); + } else { + panic!("Expected Date mention data"); + } + } + + #[test] + fn test_external_link_mention() { + let delta = build_mention_external_link_delta("https://example.com"); + + assert!(is_mention(&delta)); + assert_eq!( + extract_mention_type(&delta), + Some("externalLink".to_string()) + ); + assert_eq!(extract_url(&delta), Some("https://example.com".to_string())); + + if let Some(MentionData::ExternalLink { url }) = extract_mention_data(&delta) { + assert_eq!(url, "https://example.com"); + } else { + panic!("Expected ExternalLink mention data"); + } + } + + #[test] + fn test_non_mention_delta() { + let delta = TextDelta::Inserted("Regular text".to_string(), None); + + assert!(!is_mention(&delta)); + assert_eq!(extract_mention_type(&delta), None); + assert_eq!(extract_mention_data(&delta), None); + } +} diff --git a/collab/src/document/blocks/mod.rs b/collab/src/document/blocks/mod.rs index 2e4776af3..4d61707d9 100644 --- a/collab/src/document/blocks/mod.rs +++ b/collab/src/document/blocks/mod.rs @@ -3,6 +3,7 @@ mod block; mod block_types; mod children; mod entities; +mod mention_helper; mod text; mod text_entities; mod utils; @@ -12,6 +13,7 @@ pub use block::*; pub use block_types::*; pub use children::*; pub use entities::*; +pub use mention_helper::*; pub use text::*; pub use text_entities::*; pub use utils::*; diff --git a/collab/tests/document/blocks/mention_integration_test.rs b/collab/tests/document/blocks/mention_integration_test.rs new file mode 100644 index 000000000..4ea24180c --- /dev/null +++ b/collab/tests/document/blocks/mention_integration_test.rs @@ -0,0 +1,367 @@ +use collab::document::blocks::*; +use collab::preclude::*; + +#[test] +fn test_create_document_with_all_mention_types() { + let doc_id = "mention_integration_test"; + let test = crate::util::DocumentTest::new(1, doc_id); + let mut document = test.document; + let page_id = document.get_page_id().unwrap(); + + // Create a paragraph with mixed content and all mention types + let block_id = nanoid::nanoid!(6); + let text_id = nanoid::nanoid!(6); + + let block = Block { + id: block_id.clone(), + ty: "paragraph".to_owned(), + parent: page_id.clone(), + children: "".to_string(), + external_id: Some(text_id.clone()), + external_type: Some("text".to_owned()), + data: Default::default(), + }; + + document.insert_block(block, None).unwrap(); + + // Build a rich delta with all mention types + let mut deltas = Vec::new(); + + // Regular text + deltas.push(TextDelta::Inserted("Hello ".to_string(), None)); + + // Person mention + deltas.push(build_mention_person_delta( + "person_123", + "Alice", + &page_id, + Some(&block_id), + None, + )); + + // More text + deltas.push(TextDelta::Inserted("! Check ".to_string(), None)); + + // Page mention + deltas.push(build_mention_page_delta( + MentionPageType::Page, + "page_456", + Some("block_789"), + None, + )); + + // Text + deltas.push(TextDelta::Inserted(" and ".to_string(), None)); + + // Child page mention + deltas.push(build_mention_page_delta( + MentionPageType::ChildPage, + "child_page_999", + None, + None, + )); + + // Text + deltas.push(TextDelta::Inserted(" on ".to_string(), None)); + + // Date mention with reminder + deltas.push(build_mention_date_delta( + "2025-01-30T10:00:00Z", + Some("reminder_abc"), + Some("atTimeOfEvent"), + true, + )); + + // Text + deltas.push(TextDelta::Inserted(". Link: ".to_string(), None)); + + // External link mention + deltas.push(build_mention_external_link_delta("https://appflowy.io")); + + // Apply deltas to document + let delta_json = serde_json::to_string(&deltas).unwrap(); + document.apply_text_delta(&text_id, delta_json); + + // Read back and verify all mentions + let retrieved_deltas = document.get_block_delta(&block_id).unwrap().1; + + // Count mentions + let mut person_count = 0; + let mut page_count = 0; + let mut child_page_count = 0; + let mut date_count = 0; + let mut link_count = 0; + + for delta in &retrieved_deltas { + match extract_mention_data(delta) { + Some(MentionData::Person { + person_id, + person_name, + page_id: pid, + block_id: bid, + .. + }) => { + person_count += 1; + assert_eq!(person_id, "person_123"); + assert_eq!(person_name, "Alice"); + assert_eq!(pid, page_id); + assert_eq!(bid, Some(block_id.clone())); + }, + Some(MentionData::Page { + page_id: pid, + block_id: bid, + .. + }) => { + page_count += 1; + assert_eq!(pid, "page_456"); + assert_eq!(bid, Some("block_789".to_string())); + }, + Some(MentionData::ChildPage { page_id: pid }) => { + child_page_count += 1; + assert_eq!(pid, "child_page_999"); + }, + Some(MentionData::Date { + date, + include_time, + reminder_id, + reminder_option, + }) => { + date_count += 1; + assert_eq!(date, "2025-01-30T10:00:00Z"); + assert!(include_time); + assert_eq!(reminder_id, Some("reminder_abc".to_string())); + assert_eq!(reminder_option, Some("atTimeOfEvent".to_string())); + }, + Some(MentionData::ExternalLink { url }) => { + link_count += 1; + assert_eq!(url, "https://appflowy.io"); + }, + None => { + // Regular text, skip + }, + } + } + + assert_eq!(person_count, 1, "Should have 1 person mention"); + assert_eq!(page_count, 1, "Should have 1 page mention"); + assert_eq!(child_page_count, 1, "Should have 1 child page mention"); + assert_eq!(date_count, 1, "Should have 1 date mention"); + assert_eq!(link_count, 1, "Should have 1 external link mention"); +} + +#[test] +fn test_mention_extraction_functions() { + // Test person mention extraction + let person_delta = build_mention_person_delta("user1", "Bob", "doc1", None, Some("row1")); + assert!(is_mention(&person_delta)); + assert_eq!( + extract_mention_type(&person_delta), + Some("person".to_string()) + ); + assert_eq!(extract_person_id(&person_delta), Some("user1".to_string())); + + // Test page mention extraction + let page_delta = build_mention_page_delta(MentionPageType::Page, "page1", None, None); + assert!(is_mention(&page_delta)); + assert_eq!(extract_mention_type(&page_delta), Some("page".to_string())); + assert_eq!(extract_page_id(&page_delta), Some("page1".to_string())); + + // Test date mention extraction + let date_delta = build_mention_date_delta("2025-02-01T00:00:00Z", None, None, false); + assert!(is_mention(&date_delta)); + assert_eq!(extract_mention_type(&date_delta), Some("date".to_string())); + assert_eq!( + extract_date(&date_delta), + Some("2025-02-01T00:00:00Z".to_string()) + ); + + // Test external link extraction + let link_delta = build_mention_external_link_delta("https://rust-lang.org"); + assert!(is_mention(&link_delta)); + assert_eq!( + extract_mention_type(&link_delta), + Some("externalLink".to_string()) + ); + assert_eq!( + extract_url(&link_delta), + Some("https://rust-lang.org".to_string()) + ); + + // Test non-mention delta + let regular_delta = TextDelta::Inserted("Not a mention".to_string(), None); + assert!(!is_mention(®ular_delta)); + assert_eq!(extract_mention_type(®ular_delta), None); +} + +#[test] +fn test_flutter_compatibility() { + // This test verifies that the Rust helpers produce data structures + // identical to Flutter's buildMentionXXXAttributes functions + + // Person mention - Flutter: MentionBlockKeys.buildMentionPersonAttributes + let person_delta = build_mention_person_delta( + "person_id_123", + "John Smith", + "page_id_456", + Some("block_id_789"), + Some("row_id_abc"), + ); + + if let TextDelta::Inserted(text, Some(attrs)) = &person_delta { + assert_eq!(text, "$", "Should use $ as mention character"); + + // Verify mention structure + if let Some(Any::Map(mention)) = attrs.get("mention") { + assert_eq!(mention.get("type").unwrap().to_string(), "person"); + assert_eq!( + mention.get("person_id").unwrap().to_string(), + "person_id_123" + ); + assert_eq!( + mention.get("person_name").unwrap().to_string(), + "John Smith" + ); + assert_eq!(mention.get("page_id").unwrap().to_string(), "page_id_456"); + assert_eq!(mention.get("block_id").unwrap().to_string(), "block_id_789"); + assert_eq!(mention.get("row_id").unwrap().to_string(), "row_id_abc"); + } else { + panic!("Expected mention map in attributes"); + } + } else { + panic!("Expected Inserted delta with attributes"); + } + + // Date mention - Flutter: MentionBlockKeys.buildMentionDateAttributes + let date_delta = build_mention_date_delta( + "2025-01-30T12:00:00.000Z", + Some("reminder_xyz"), + Some("atTimeOfEvent"), + true, + ); + + if let TextDelta::Inserted(text, Some(attrs)) = &date_delta { + assert_eq!(text, "$"); + + if let Some(Any::Map(mention)) = attrs.get("mention") { + assert_eq!(mention.get("type").unwrap().to_string(), "date"); + assert_eq!( + mention.get("date").unwrap().to_string(), + "2025-01-30T12:00:00.000Z" + ); + assert_eq!(mention.get("include_time").unwrap().to_string(), "true"); + assert_eq!( + mention.get("reminder_id").unwrap().to_string(), + "reminder_xyz" + ); + assert_eq!( + mention.get("reminder_option").unwrap().to_string(), + "atTimeOfEvent" + ); + } else { + panic!("Expected mention map in attributes"); + } + } else { + panic!("Expected Inserted delta with attributes"); + } +} + +#[test] +fn test_mention_data_enum() { + // Test extracting complete mention data using the enum + + let person = build_mention_person_delta("p1", "Alice", "d1", None, None); + match extract_mention_data(&person) { + Some(MentionData::Person { + person_id, + person_name, + .. + }) => { + assert_eq!(person_id, "p1"); + assert_eq!(person_name, "Alice"); + }, + _ => panic!("Expected Person mention data"), + } + + let page = build_mention_page_delta(MentionPageType::Page, "p2", Some("b1"), None); + match extract_mention_data(&page) { + Some(MentionData::Page { + page_id, block_id, .. + }) => { + assert_eq!(page_id, "p2"); + assert_eq!(block_id, Some("b1".to_string())); + }, + _ => panic!("Expected Page mention data"), + } + + let child = build_mention_page_delta(MentionPageType::ChildPage, "p3", None, None); + match extract_mention_data(&child) { + Some(MentionData::ChildPage { page_id }) => { + assert_eq!(page_id, "p3"); + }, + _ => panic!("Expected ChildPage mention data"), + } + + let date = build_mention_date_delta("2025-01-30T10:00:00Z", None, None, false); + match extract_mention_data(&date) { + Some(MentionData::Date { + date, include_time, .. + }) => { + assert_eq!(date, "2025-01-30T10:00:00Z"); + assert_eq!(include_time, false); + }, + _ => panic!("Expected Date mention data"), + } + + let link = build_mention_external_link_delta("https://appflowy.io"); + match extract_mention_data(&link) { + Some(MentionData::ExternalLink { url }) => { + assert_eq!(url, "https://appflowy.io"); + }, + _ => panic!("Expected ExternalLink mention data"), + } +} + +#[test] +fn test_date_without_time() { + // Flutter often sends dates without time + let date_delta = build_mention_date_delta("2025-01-30T00:00:00Z", None, None, false); + + match extract_mention_data(&date_delta) { + Some(MentionData::Date { + date, + include_time, + reminder_id, + reminder_option, + }) => { + assert_eq!(date, "2025-01-30T00:00:00Z"); + assert_eq!(include_time, false); + assert_eq!(reminder_id, None); + assert_eq!(reminder_option, None); + }, + _ => panic!("Expected Date mention data"), + } +} + +#[test] +fn test_page_with_block_deep_link() { + // Test page mention with block ID for deep linking + let page_delta = build_mention_page_delta( + MentionPageType::Page, + "target_page", + Some("target_block"), + None, + ); + + match extract_mention_data(&page_delta) { + Some(MentionData::Page { + page_id, + block_id, + row_id, + }) => { + assert_eq!(page_id, "target_page"); + assert_eq!(block_id, Some("target_block".to_string())); + assert_eq!(row_id, None); + }, + _ => panic!("Expected Page mention data with block_id"), + } +} diff --git a/collab/tests/document/blocks/mod.rs b/collab/tests/document/blocks/mod.rs index cbeda8d00..106d425df 100644 --- a/collab/tests/document/blocks/mod.rs +++ b/collab/tests/document/blocks/mod.rs @@ -1,3 +1,4 @@ mod block_test; pub mod block_test_core; +mod mention_integration_test; mod text_test; diff --git a/collab/tests/document/main.rs b/collab/tests/document/main.rs index 0ecf003c8..0c2128c14 100644 --- a/collab/tests/document/main.rs +++ b/collab/tests/document/main.rs @@ -2,11 +2,8 @@ mod block_parser; mod blocks; -mod document; -mod util; - mod conversions; - +mod document; mod importer; - mod remapper; +mod util;