use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; use super::{Block, Page, PagePermissions, PropertyValue, WorkspaceIcon, WorkspaceRole}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Breadcrumb { pub page_id: Uuid, pub title: String, pub icon: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageBreadcrumbs { pub workspace_id: Uuid, pub workspace_name: String, pub workspace_icon: Option, pub path: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageSummary { pub id: Uuid, pub title: String, pub icon: Option, pub parent_id: Option, pub has_children: bool, pub created_at: DateTime, pub updated_at: DateTime, pub created_by: Uuid, pub last_edited_by: Uuid, } impl From<&Page> for PageSummary { fn from(page: &Page) -> Self { Self { id: page.id, title: page.title.clone(), icon: page.icon.clone(), parent_id: page.parent_id, has_children: !page.children.is_empty(), created_at: page.created_at, updated_at: page.updated_at, created_by: page.created_by, last_edited_by: page.last_edited_by, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreatePageRequest { pub workspace_id: Uuid, pub parent_id: Option, pub title: String, pub icon: Option, pub cover_image: Option, pub template_id: Option, pub properties: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdatePageRequest { pub title: Option, pub icon: Option, pub cover_image: Option, pub properties: Option>, pub permissions: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MovePageRequest { pub new_parent_id: Option, pub new_workspace_id: Option, pub position: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DuplicatePageRequest { pub new_parent_id: Option, pub new_workspace_id: Option, pub include_children: bool, pub new_title: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageExportOptions { pub format: ExportFormat, pub include_children: bool, pub include_images: bool, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ExportFormat { Markdown, Html, Pdf, PlainText, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageImportOptions { pub format: ImportFormat, pub parent_id: Option, pub workspace_id: Uuid, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ImportFormat { Markdown, Html, Notion, Confluence, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecentPage { pub page_id: Uuid, pub workspace_id: Uuid, pub title: String, pub icon: Option, pub workspace_name: String, pub accessed_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FavoritePage { pub page_id: Uuid, pub workspace_id: Uuid, pub title: String, pub icon: Option, pub added_at: DateTime, } pub fn build_breadcrumbs( page_id: Uuid, pages: &HashMap, workspace_name: &str, workspace_icon: Option, workspace_id: Uuid, ) -> PageBreadcrumbs { let mut path = Vec::new(); let mut current_id = Some(page_id); while let Some(id) = current_id { if let Some(page) = pages.get(&id) { path.push(Breadcrumb { page_id: page.id, title: page.title.clone(), icon: page.icon.clone(), }); current_id = page.parent_id; } else { break; } } path.reverse(); PageBreadcrumbs { workspace_id, workspace_name: workspace_name.to_string(), workspace_icon, path, } } pub fn get_page_depth(page_id: Uuid, pages: &HashMap) -> usize { let mut depth = 0; let mut current_id = Some(page_id); while let Some(id) = current_id { if let Some(page) = pages.get(&id) { depth += 1; current_id = page.parent_id; } else { break; } } depth } pub fn get_all_descendants(page_id: Uuid, pages: &HashMap) -> Vec { let mut descendants = Vec::new(); if let Some(page) = pages.get(&page_id) { for child_id in &page.children { descendants.push(*child_id); descendants.extend(get_all_descendants(*child_id, pages)); } } descendants } pub fn get_all_ancestors(page_id: Uuid, pages: &HashMap) -> Vec { let mut ancestors = Vec::new(); let mut current_id = pages.get(&page_id).and_then(|p| p.parent_id); while let Some(id) = current_id { ancestors.push(id); current_id = pages.get(&id).and_then(|p| p.parent_id); } ancestors } pub fn is_descendant_of(page_id: Uuid, potential_ancestor: Uuid, pages: &HashMap) -> bool { let ancestors = get_all_ancestors(page_id, pages); ancestors.contains(&potential_ancestor) } pub fn can_move_page( page_id: Uuid, new_parent_id: Option, pages: &HashMap, ) -> Result<(), String> { if let Some(new_pid) = new_parent_id { if page_id == new_pid { return Err("Cannot move page into itself".to_string()); } if is_descendant_of(new_pid, page_id, pages) { return Err("Cannot move page into its own descendant".to_string()); } } Ok(()) } pub fn check_page_permission( page: &Page, user_id: Uuid, user_role: WorkspaceRole, required_permission: PagePermissionType, ) -> bool { if page.permissions.public { match required_permission { PagePermissionType::View => return true, PagePermissionType::Edit => { if page.permissions.public_edit { return true; } } _ => {} } } if page.permissions.allowed_users.contains(&user_id) { return true; } if page.permissions.allowed_roles.contains(&user_role) { return true; } match user_role { WorkspaceRole::Owner | WorkspaceRole::Admin => true, WorkspaceRole::Editor => matches!( required_permission, PagePermissionType::View | PagePermissionType::Edit | PagePermissionType::Comment ), WorkspaceRole::Commenter => matches!( required_permission, PagePermissionType::View | PagePermissionType::Comment ), WorkspaceRole::Viewer => matches!(required_permission, PagePermissionType::View), } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PagePermissionType { View, Edit, Comment, Share, Delete, } pub fn duplicate_page( page: &Page, new_parent_id: Option, new_workspace_id: Option, new_title: Option, created_by: Uuid, pages: &HashMap, include_children: bool, ) -> Vec { let mut duplicated_pages = Vec::new(); let now = Utc::now(); let new_page = Page { id: Uuid::new_v4(), workspace_id: new_workspace_id.unwrap_or(page.workspace_id), parent_id: new_parent_id, title: new_title.unwrap_or_else(|| format!("{} (Copy)", page.title)), icon: page.icon.clone(), cover_image: page.cover_image.clone(), blocks: page.blocks.clone(), children: Vec::new(), properties: page.properties.clone(), permissions: PagePermissions::default(), is_template: false, template_id: page.template_id, created_at: now, updated_at: now, created_by, last_edited_by: created_by, }; let new_page_id = new_page.id; duplicated_pages.push(new_page); if include_children { for child_id in &page.children { if let Some(child_page) = pages.get(child_id) { let child_duplicates = duplicate_page( child_page, Some(new_page_id), new_workspace_id, None, created_by, pages, true, ); duplicated_pages.extend(child_duplicates); } } } duplicated_pages } pub fn sort_pages_by_title(pages: &mut [PageSummary]) { pages.sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())); } pub fn sort_pages_by_updated(pages: &mut [PageSummary]) { pages.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); } pub fn sort_pages_by_created(pages: &mut [PageSummary]) { pages.sort_by(|a, b| b.created_at.cmp(&a.created_at)); } pub fn filter_pages_by_date_range( pages: Vec, start: Option>, end: Option>, ) -> Vec { pages .into_iter() .filter(|p| { let after_start = start.map(|s| p.updated_at >= s).unwrap_or(true); let before_end = end.map(|e| p.updated_at <= e).unwrap_or(true); after_start && before_end }) .collect() } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageStats { pub total_blocks: usize, pub total_words: usize, pub total_characters: usize, pub has_images: bool, pub has_tables: bool, pub has_code: bool, pub child_count: usize, pub comment_count: usize, } pub fn calculate_page_stats(page: &Page, comment_count: usize) -> PageStats { let mut stats = PageStats { total_blocks: 0, total_words: 0, total_characters: 0, has_images: false, has_tables: false, has_code: false, child_count: page.children.len(), comment_count, }; count_blocks_stats(&page.blocks, &mut stats); stats } fn count_blocks_stats(blocks: &[Block], stats: &mut PageStats) { use super::{BlockContent, BlockType}; for block in blocks { stats.total_blocks += 1; match block.block_type { BlockType::Image => stats.has_images = true, BlockType::Table => stats.has_tables = true, BlockType::Code => stats.has_code = true, _ => {} } if let BlockContent::Text { text: rich_text } = &block.content { for segment in &rich_text.segments { stats.total_characters += segment.text.len(); stats.total_words += segment.text.split_whitespace().count(); } } count_blocks_stats(&block.children, stats); } }