diff --git a/collab/src/folder/folder.rs b/collab/src/folder/folder.rs index 73d6f3be4..385a8ce3a 100644 --- a/collab/src/folder/folder.rs +++ b/collab/src/folder/folder.rs @@ -157,20 +157,26 @@ impl Folder { /// /// * `Some(FolderData)`: If the operation is successful, it returns `Some` variant wrapping `FolderData` /// object, which consists of current workspace ID, current view, a list of workspaces, and their respective views. + /// When uid is provided, includes user-specific sections. When uid is None, returns empty user-specific sections. /// /// * `None`: If the operation is unsuccessful (though it should typically not be the case as `Some` /// is returned explicitly), it returns `None`. - pub fn get_folder_data(&self, workspace_id: &str, uid: i64) -> Option { + pub fn get_folder_data(&self, workspace_id: &str, uid: Option) -> Option { let txn = self.collab.transact(); self.body.get_folder_data(&txn, workspace_id, uid) } - /// Fetches the current workspace. + /// Fetches the current workspace. The uid parameter is accepted for API consistency + /// but not used as workspace data is shared across all users. /// /// This function fetches the ID of the current workspace from the meta object, /// and uses this ID to fetch the actual workspace object. /// - pub fn get_workspace_info(&self, workspace_id: &WorkspaceId, uid: i64) -> Option { + pub fn get_workspace_info( + &self, + workspace_id: &WorkspaceId, + uid: Option, + ) -> Option { let txn = self.collab.transact(); self.body.get_workspace_info(&txn, workspace_id, uid) } @@ -180,24 +186,30 @@ impl Folder { self.body.get_workspace_id(&txn)?.parse().ok() } - pub fn get_all_views(&self, uid: i64) -> Vec> { + /// Get all views. When uid is provided, includes user-specific data like is_favorite. + /// When uid is None, returns base view data without user-specific enrichment. + pub fn get_all_views(&self, uid: Option) -> Vec> { let txn = self.collab.transact(); self.body.views.get_all_views(&txn, uid) } - pub fn get_views(&self, view_ids: &[ViewId], uid: i64) -> Vec> { + /// Get multiple views by ids. When uid is provided, includes user-specific data like is_favorite. + /// When uid is None, returns base view data without user-specific enrichment. + pub fn get_views(&self, view_ids: &[ViewId], uid: Option) -> Vec> { let txn = self.collab.transact(); self.body.views.get_views(&txn, view_ids, uid) } - pub fn get_views_belong_to(&self, parent_id: &ViewId, uid: i64) -> Vec> { + /// Get all views belonging to a parent. When uid is provided, includes user-specific data. + /// When uid is None, returns base view data without user-specific enrichment. + pub fn get_views_belong_to(&self, parent_id: &ViewId, uid: Option) -> Vec> { let txn = self.collab.transact(); self.body.views.get_views_belong_to(&txn, parent_id, uid) } pub fn move_view(&mut self, view_id: &ViewId, from: u32, to: u32, uid: i64) -> Option> { let mut txn = self.collab.transact_mut(); - self.body.move_view(&mut txn, view_id, from, to, uid) + self.body.move_view(&mut txn, view_id, from, to, Some(uid)) } /// Moves a nested view to a new location in the hierarchy. @@ -227,17 +239,17 @@ impl Folder { let mut txn = self.collab.transact_mut(); self .body - .move_nested_view(&mut txn, view_id, new_parent_id, prev_view_id, uid) + .move_nested_view(&mut txn, view_id, new_parent_id, prev_view_id, Some(uid)) } pub fn set_current_view(&mut self, view_id: ViewId, uid: i64) { let mut txn = self.collab.transact_mut(); - self.body.set_current_view(&mut txn, view_id, uid); + self.body.set_current_view(&mut txn, view_id, Some(uid)); } pub fn get_current_view(&self, uid: i64) -> Option { let txn = self.collab.transact(); - self.body.get_current_view(&txn, uid) + self.body.get_current_view(&txn, Some(uid)) } pub fn update_view(&mut self, view_id: &ViewId, f: F, uid: i64) -> Option> @@ -283,17 +295,175 @@ impl Folder { } } - pub fn get_my_favorite_sections(&self, uid: i64) -> Vec { + /// Retrieves the favorite views for a specific user. + /// + /// # How Sections Work + /// + /// Sections are **user-specific collections** stored in the collaborative folder. + /// The folder maintains four predefined sections: Favorite, Recent, Trash, and Private. + /// + /// **Data Structure:** + /// ```text + /// Folder (CRDT) + /// └─ SectionMap + /// └─ "favorite" (Section) + /// ├─ "1" (uid) → [SectionItem { id: view_uuid, timestamp }, ...] + /// ├─ "2" (uid) → [SectionItem { id: view_uuid, timestamp }, ...] + /// └─ "3" (uid) → [SectionItem { id: view_uuid, timestamp }, ...] + /// ``` + /// + /// Each section type (favorite/recent/trash/private) contains a map where: + /// - **Key**: User ID (as string representation of i64) + /// - **Value**: Array of `SectionItem` structs, each containing: + /// - `id`: ViewId (UUID) of the view in this section + /// - `timestamp`: When the view was added to this section + /// + /// This architecture allows multiple users to collaborate on the same folder + /// while maintaining separate personal collections (favorites, recent views, etc.). + /// + /// # Parameters + /// + /// * `uid` - Optional user ID to query favorites for + /// - `Some(uid)`: Returns the favorite views for the specified user + /// - `None`: Returns an empty vector (no user context = no user-specific data) + /// + /// # Returns + /// + /// A vector of `SectionItem` structs representing the user's favorite views. + /// Each item contains the view's UUID and the timestamp when it was favorited. + /// Returns empty vector if: + /// - `uid` is `None` + /// - The user has no favorites + /// - The favorite section doesn't exist + /// + /// # Why `Option` for Query Operations? + /// + /// This method uses `Option` (not required `i64`) because: + /// 1. **Safe degradation**: Can return meaningful result (empty) when uid is unknown + /// 2. **Flexible usage**: Callers can query without having user context + /// 3. **No side effects**: Read-only operation that doesn't modify data + /// + /// Compare with mutation operations like `add_favorite_view_ids(uid: i64)` which + /// require `i64` because adding favorites without a user ID would be meaningless. + /// + /// # Related Methods + /// + /// - [`get_all_favorites_sections`]: Gets favorites across all users (admin mode) + /// - [`add_favorite_view_ids`]: Adds views to user's favorites (requires `i64`) + /// - [`delete_favorite_view_ids`]: Removes views from user's favorites (requires `i64`) + /// - [`get_my_trash_sections`]: Similar pattern for trash section + /// - [`get_my_private_sections`]: Similar pattern for private section + /// - [`get_my_recent_sections`]: Similar pattern for recent section + /// + /// # Examples + /// + /// ```rust,ignore + /// // Get favorites for a specific user + /// let user_id = UserId::from(123); + /// let favorites = folder.get_my_favorite_sections(Some(user_id.as_i64())); + /// for item in favorites { + /// println!("View {} was favorited at {}", item.id, item.timestamp); + /// } + /// + /// // Query without user context (returns empty) + /// let no_favorites = folder.get_my_favorite_sections(None); + /// assert!(no_favorites.is_empty()); + /// + /// // Check if a view is favorited + /// let is_favorited = folder.get_my_favorite_sections(Some(uid)) + /// .iter() + /// .any(|item| item.id == target_view_id); + /// ``` + /// + /// # Implementation Details + /// + /// Internally, this method: + /// 1. Creates a read transaction on the Collab CRDT + /// 2. Gets a `SectionOperation` for the Favorite section with the given uid + /// 3. Calls `get_all_section_item()` which: + /// - Looks up the array at key `uid.to_string()` in the favorite section + /// - Deserializes each Yrs array element into a `SectionItem` + /// - Returns the vector of items + /// + /// The operation is **read-only** and **lock-free** thanks to CRDT properties. + pub fn get_my_favorite_sections(&self, uid: Option) -> Vec { + let Some(uid) = uid else { + return vec![]; + }; let txn = self.collab.transact(); self .body .section - .section_op(&txn, Section::Favorite, uid) + .section_op(&txn, Section::Favorite, Some(uid)) .map(|op| op.get_all_section_item(&txn)) .unwrap_or_default() } - pub fn get_all_favorites_sections(&self, uid: i64) -> Vec { + /// Retrieves favorite views across all users, with optional filtering by user. + /// + /// This is the "admin mode" variant of [`get_my_favorite_sections`]. While `get_my_*` + /// returns empty when uid is None, this method returns **all users' favorites** when + /// uid is None. + /// + /// # Parameters + /// + /// * `uid` - Optional user ID for filtering + /// - `Some(uid)`: Returns only the favorites for the specified user (same as `get_my_favorite_sections`) + /// - `None`: Returns favorites from **all users** (admin/global query mode) + /// + /// # Returns + /// + /// A flattened vector of all `SectionItem` entries across the queried user(s). + /// + /// # Behavior When uid is None + /// + /// **This is the key difference from `get_my_favorite_sections`:** + /// + /// When `uid` is `None`, this method returns favorites from **all users**, not an empty vector. + /// This enables admin/debugging operations like: + /// - Viewing all favorited content across the workspace + /// - Finding popular/frequently favorited views + /// - Debugging favorite state + /// + /// ```text + /// get_my_favorite_sections(None) → [] (empty - no user context) + /// get_all_favorites_sections(None) → [user1's favorites, user2's favorites, ...] (all users) + /// ``` + /// + /// # Use Cases + /// + /// ## Admin Dashboard - Popular Views + /// ```rust,ignore + /// // Get all favorited views across all users + /// let all_favorites = folder.get_all_favorites_sections(None); + /// let view_counts: HashMap = all_favorites + /// .iter() + /// .fold(HashMap::new(), |mut map, item| { + /// *map.entry(item.id).or_insert(0) += 1; + /// map + /// }); + /// + /// // Find most favorited views + /// let popular = view_counts + /// .into_iter() + /// .filter(|(_, count)| *count >= 3) + /// .collect::>(); + /// ``` + /// + /// ## Check Specific User (Alternative to get_my_*) + /// ```rust,ignore + /// // These are equivalent: + /// let favorites_a = folder.get_my_favorite_sections(Some(uid)); + /// let favorites_b = folder.get_all_favorites_sections(Some(uid)); + /// assert_eq!(favorites_a, favorites_b); + /// ``` + /// + /// # Related Methods + /// + /// - [`get_my_favorite_sections`]: User-scoped version (returns empty when uid is None) + /// - [`add_favorite_view_ids`]: Add favorites for a user (requires `i64`) + /// - [`delete_favorite_view_ids`]: Remove favorites for a user (requires `i64`) + pub fn get_all_favorites_sections(&self, uid: Option) -> Vec { let txn = self.collab.transact(); self .body @@ -308,14 +478,22 @@ impl Folder { pub fn remove_all_my_favorite_sections(&mut self, uid: i64) { let mut txn = self.collab.transact_mut(); - if let Some(op) = self.body.section.section_op(&txn, Section::Favorite, uid) { + if let Some(op) = self + .body + .section + .section_op(&txn, Section::Favorite, Some(uid)) + { op.clear(&mut txn); } } pub fn move_favorite_view_id(&mut self, id: &str, prev_id: Option<&str>, uid: i64) { let mut txn = self.collab.transact_mut(); - if let Some(op) = self.body.section.section_op(&txn, Section::Favorite, uid) { + if let Some(op) = self + .body + .section + .section_op(&txn, Section::Favorite, Some(uid)) + { op.move_section_item_with_txn(&mut txn, id, prev_id); } } @@ -349,17 +527,155 @@ impl Folder { } } - pub fn get_my_trash_sections(&self, uid: i64) -> Vec { + /// Retrieves the trashed views for a specific user. + /// + /// The trash section contains views that have been deleted by the user but not yet + /// permanently removed. This allows for a "trash bin" functionality where users can + /// recover accidentally deleted views. + /// + /// # Parameters + /// + /// * `uid` - Optional user ID to query trash for + /// - `Some(uid)`: Returns the trashed views for the specified user + /// - `None`: Returns an empty vector (no user = no trash to show) + /// + /// # Returns + /// + /// A vector of `SectionItem` structs where: + /// - `item.id`: The view UUID that was trashed + /// - `item.timestamp`: When the view was moved to trash (for auto-deletion policies) + /// + /// Returns empty vector if: + /// - `uid` is `None` (no user context) + /// - The user has no items in trash + /// - The trash section doesn't exist + /// + /// # Behavior When uid is None + /// + /// When `uid` is `None`, this method returns an empty vector because trash is + /// **user-specific** data. Without knowing which user's trash to query, the method + /// cannot return meaningful results. This design: + /// - Prevents accidentally showing another user's deleted items + /// - Fails safely (empty instead of error) + /// - Maintains consistency with other user-scoped queries + /// + /// If you need to query trash across all users (e.g., for admin purposes), use + /// [`get_all_trash_sections`] instead. + /// + /// # Common Use Cases + /// + /// ## Display Trash Bin + /// ```rust,ignore + /// let trash_items = folder.get_my_trash_sections(Some(current_user_id)); + /// for item in trash_items { + /// show_trashed_view(item.id, item.timestamp); + /// } + /// ``` + /// + /// ## Auto-Delete Old Items + /// ```rust,ignore + /// let trash = folder.get_my_trash_sections(Some(uid)); + /// let thirty_days_ago = current_timestamp() - (30 * 24 * 60 * 60 * 1000); + /// + /// let to_permanently_delete: Vec<_> = trash + /// .iter() + /// .filter(|item| item.timestamp < thirty_days_ago) + /// .map(|item| item.id.to_string()) + /// .collect(); + /// + /// if !to_permanently_delete.is_empty() { + /// folder.delete_trash_view_ids(to_permanently_delete, uid); + /// } + /// ``` + /// + /// ## Check if View is in Trash + /// ```rust,ignore + /// let is_trashed = folder.get_my_trash_sections(Some(uid)) + /// .iter() + /// .any(|item| item.id == target_view_id); + /// ``` + /// + /// # Related Methods + /// + /// - [`add_trash_view_ids`]: Move views to trash (requires `i64`) + /// - [`delete_trash_view_ids`]: Permanently delete from trash (requires `i64`) + /// - [`get_all_trash_sections`]: Query trash across all users (admin mode) + /// - [`get_my_trash_info`]: Get trash with additional view metadata + pub fn get_my_trash_sections(&self, uid: Option) -> Vec { + let Some(uid) = uid else { + return vec![]; + }; let txn = self.collab.transact(); self .body .section - .section_op(&txn, Section::Trash, uid) + .section_op(&txn, Section::Trash, Some(uid)) .map(|op| op.get_all_section_item(&txn)) .unwrap_or_default() } - pub fn get_all_trash_sections(&self, uid: i64) -> Vec { + /// Retrieves trashed views across all users, with optional filtering by user. + /// + /// This is the "admin mode" variant of [`get_my_trash_sections`]. While `get_my_trash_sections` + /// returns empty when uid is None, this method returns **all users' trash** when uid is None. + /// + /// # Parameters + /// + /// * `uid` - Optional user ID for filtering + /// - `Some(uid)`: Returns only the trash for the specified user + /// - `None`: Returns trash from **all users** (admin/cleanup mode) + /// + /// # Returns + /// + /// A flattened vector of all trashed `SectionItem` entries across the queried user(s). + /// + /// # Behavior When uid is None + /// + /// When `uid` is `None`, this method returns trash items from **all users**: + /// + /// ```text + /// get_my_trash_sections(None) → [] (empty - no user context) + /// get_all_trash_sections(None) → [user1's trash, user2's trash, ...] (all users) + /// ``` + /// + /// This is useful for: + /// - Admin cleanup operations (find all deleted content) + /// - Global auto-deletion policies (remove items older than N days across all users) + /// - Debugging trash state + /// - Recovering content when user ID is unknown + /// + /// # Use Cases + /// + /// ## Global Cleanup - Delete Old Trash + /// ```rust,ignore + /// // Find all trash items older than 30 days across ALL users + /// let all_trash = folder.get_all_trash_sections(None); + /// let thirty_days_ago = current_timestamp() - (30 * 24 * 60 * 60 * 1000); + /// + /// let old_trash: Vec<_> = all_trash + /// .into_iter() + /// .filter(|item| item.timestamp < thirty_days_ago) + /// .collect(); + /// + /// // Note: Actual deletion requires uid per item, so you'd need to + /// // track which user owns which trash item separately + /// ``` + /// + /// ## Admin Dashboard - Trash Statistics + /// ```rust,ignore + /// let all_trash = folder.get_all_trash_sections(None); + /// println!("Total items in trash across all users: {}", all_trash.len()); + /// + /// // Calculate trash size per user (requires additional tracking) + /// ``` + /// + /// # Related Methods + /// + /// - [`get_my_trash_sections`]: User-scoped version (returns empty when uid is None) + /// - [`get_my_trash_info`]: User-scoped with view names + /// - [`add_trash_view_ids`]: Move views to trash (requires `i64`) + /// - [`delete_trash_view_ids`]: Permanently delete from trash (requires `i64`) + pub fn get_all_trash_sections(&self, uid: Option) -> Vec { let txn = self.collab.transact(); self .body @@ -374,14 +690,22 @@ impl Folder { pub fn remove_all_my_trash_sections(&mut self, uid: i64) { let mut txn = self.collab.transact_mut(); - if let Some(op) = self.body.section.section_op(&txn, Section::Trash, uid) { + if let Some(op) = self + .body + .section + .section_op(&txn, Section::Trash, Some(uid)) + { op.clear(&mut txn); } } pub fn move_trash_view_id(&mut self, id: &str, prev_id: Option<&str>, uid: i64) { let mut txn = self.collab.transact_mut(); - if let Some(op) = self.body.section.section_op(&txn, Section::Trash, uid) { + if let Some(op) = self + .body + .section + .section_op(&txn, Section::Trash, Some(uid)) + { op.move_section_item_with_txn(&mut txn, id, prev_id); } } @@ -415,17 +739,166 @@ impl Folder { } } - pub fn get_my_private_sections(&self, uid: i64) -> Vec { + /// Retrieves the private views for a specific user. + /// + /// The private section contains views that are marked as private/personal to the user + /// and should be hidden from other collaborators. This enables personal workspace areas + /// within a shared collaborative folder. + /// + /// # Parameters + /// + /// * `uid` - Optional user ID to query private views for + /// - `Some(uid)`: Returns the private views for the specified user + /// - `None`: Returns an empty vector (no user = no private views to show) + /// + /// # Returns + /// + /// A vector of `SectionItem` structs where: + /// - `item.id`: The view UUID marked as private + /// - `item.timestamp`: When the view was marked as private + /// + /// Returns empty vector if: + /// - `uid` is `None` (no user context) + /// - The user has no private views + /// - The private section doesn't exist + /// + /// # Behavior When uid is None + /// + /// When `uid` is `None`, this method returns an empty vector because private views are + /// **inherently user-specific**. The concept of "private" only makes sense in the context + /// of a specific user - views are private *to someone*. Without a user ID: + /// - Cannot determine whose private views to return + /// - Returning all users' private views would violate privacy + /// - Empty result is the safest, most consistent behavior + /// + /// For admin operations that need to see all private views across users, use + /// [`get_all_private_sections`] instead. + /// + /// # Privacy Semantics + /// + /// **Important**: This method only returns which views are *marked* as private. The actual + /// visibility enforcement (hiding these views from other users) must be implemented by + /// the application layer. The section system provides the data structure, not the access + /// control mechanism. + /// + /// # Common Use Cases + /// + /// ## Filter Out Other Users' Private Views + /// ```rust,ignore + /// let all_views = folder.get_all_views(Some(current_user_id)); + /// let other_private_views = folder.get_all_private_sections(Some(current_user_id)); + /// + /// let visible_views: Vec<_> = all_views + /// .into_iter() + /// .filter(|view| { + /// !other_private_views.iter().any(|private| private.id == view.id) + /// }) + /// .collect(); + /// ``` + /// + /// ## Show User's Personal Workspace + /// ```rust,ignore + /// let my_private = folder.get_my_private_sections(Some(uid)); + /// if !my_private.is_empty() { + /// render_private_section_ui(&my_private); + /// } + /// ``` + /// + /// ## Check if View is Private + /// ```rust,ignore + /// let is_private = folder.get_my_private_sections(Some(uid)) + /// .iter() + /// .any(|item| item.id == view_id); + /// ``` + /// + /// # Related Methods + /// + /// - [`add_private_view_ids`]: Mark views as private (requires `i64`) + /// - [`delete_private_view_ids`]: Unmark views as private (requires `i64`) + /// - [`get_all_private_sections`]: Query private views across all users (admin mode) + pub fn get_my_private_sections(&self, uid: Option) -> Vec { + let Some(uid) = uid else { + return vec![]; + }; let txn = self.collab.transact(); self .body .section - .section_op(&txn, Section::Private, uid) + .section_op(&txn, Section::Private, Some(uid)) .map(|op| op.get_all_section_item(&txn)) .unwrap_or_default() } - pub fn get_all_private_sections(&self, uid: i64) -> Vec { + /// Retrieves private views across all users, with optional filtering by user. + /// + /// This is the "admin mode" variant of [`get_my_private_sections`]. While `get_my_private_sections` + /// returns empty when uid is None, this method returns **all users' private views** when uid is None. + /// + /// # Parameters + /// + /// * `uid` - Optional user ID for filtering + /// - `Some(uid)`: Returns only the private views for the specified user + /// - `None`: Returns private views from **all users** (admin/audit mode) + /// + /// # Returns + /// + /// A flattened vector of all private `SectionItem` entries across the queried user(s). + /// + /// # Behavior When uid is None + /// + /// When `uid` is `None`, this method returns private items from **all users**: + /// + /// ```text + /// get_my_private_sections(None) → [] (empty - no user context) + /// get_all_private_sections(None) → [user1's private, user2's private, ...] (all users) + /// ``` + /// + /// **Privacy Note**: Returning all users' private views may have privacy implications. + /// This method should typically only be called: + /// - In admin/debugging contexts + /// - For workspace-level operations (e.g., migration, backup) + /// - When implementing view filtering logic (to hide *other* users' private views) + /// + /// # Use Cases + /// + /// ## Filter Out Other Users' Private Views + /// ```rust,ignore + /// // Get all views, then filter out views private to other users + /// let all_views = folder.get_all_views(Some(current_user_id)); + /// let my_private_views = folder.get_my_private_sections(Some(current_user_id)); + /// let others_private_views = folder.get_all_private_sections(None); + /// + /// let visible_to_me: Vec<_> = all_views + /// .into_iter() + /// .filter(|view| { + /// // Show if it's my private view OR not private to anyone else + /// my_private_views.iter().any(|p| p.id == view.id) + /// || !others_private_views.iter().any(|p| p.id == view.id) + /// }) + /// .collect(); + /// ``` + /// + /// ## Admin Audit - Find All Private Content + /// ```rust,ignore + /// let all_private = folder.get_all_private_sections(None); + /// println!("Total private views across workspace: {}", all_private.len()); + /// + /// // Identify users with private content (requires user tracking) + /// ``` + /// + /// ## Migration/Backup Operations + /// ```rust,ignore + /// // When migrating workspace, preserve all private view metadata + /// let all_private = folder.get_all_private_sections(None); + /// save_to_backup("private_sections.json", &all_private); + /// ``` + /// + /// # Related Methods + /// + /// - [`get_my_private_sections`]: User-scoped version (returns empty when uid is None) + /// - [`add_private_view_ids`]: Mark views as private (requires `i64`) + /// - [`delete_private_view_ids`]: Unmark views as private (requires `i64`) + pub fn get_all_private_sections(&self, uid: Option) -> Vec { let txn = self.collab.transact(); self .body @@ -440,22 +913,136 @@ impl Folder { pub fn remove_all_my_private_sections(&mut self, uid: i64) { let mut txn = self.collab.transact_mut(); - if let Some(op) = self.body.section.section_op(&txn, Section::Private, uid) { + if let Some(op) = self + .body + .section + .section_op(&txn, Section::Private, Some(uid)) + { op.clear(&mut txn); } } pub fn move_private_view_id(&mut self, id: &str, prev_id: Option<&str>, uid: i64) { let mut txn = self.collab.transact_mut(); - if let Some(op) = self.body.section.section_op(&txn, Section::Private, uid) { + if let Some(op) = self + .body + .section + .section_op(&txn, Section::Private, Some(uid)) + { op.move_section_item_with_txn(&mut txn, id, prev_id); } } - pub fn get_my_trash_info(&self, uid: i64) -> Vec { + /// Retrieves enriched trash information for a specific user. + /// + /// This is an enhanced version of [`get_my_trash_sections`] that includes additional + /// view metadata (name) for each trashed item. This is useful for displaying trash bins + /// in the UI where you need to show the view name, not just its ID. + /// + /// # Parameters + /// + /// * `uid` - Optional user ID to query trash info for + /// - `Some(uid)`: Returns trash info for the specified user + /// - `None`: Returns an empty vector (no user = no trash to show) + /// + /// # Returns + /// + /// A vector of `TrashInfo` structs where each contains: + /// - `id`: ViewId (UUID) of the trashed view + /// - `name`: Human-readable name of the view (e.g., "My Document") + /// - `created_at`: Timestamp when the view was moved to trash + /// + /// Returns empty vector if: + /// - `uid` is `None` (no user context) + /// - The user has no items in trash + /// - The trash section doesn't exist + /// + /// **Note**: If a view ID exists in the trash section but the view itself has been + /// permanently deleted or its name cannot be retrieved, that item will be **omitted** + /// from the results (via `flat_map` semantics). This ensures the returned data is + /// always consistent and displayable. + /// + /// # Behavior When uid is None + /// + /// When `uid` is `None`, returns an empty vector because: + /// - Trash is user-specific data + /// - Cannot determine which user's trash to query + /// - Prevents privacy leaks (showing other users' deleted items) + /// - Consistent with [`get_my_trash_sections`] behavior + /// + /// For admin operations, use `get_my_trash_sections` with all user IDs manually, + /// as there's no `get_all_trash_info` variant (by design, to prevent accidentally + /// exposing sensitive deleted data). + /// + /// # Comparison with get_my_trash_sections + /// + /// | Method | Returns | View Name | Use When | + /// |--------|---------|-----------|----------| + /// | `get_my_trash_sections` | `Vec` | No | Need just IDs/timestamps | + /// | `get_my_trash_info` | `Vec` | Yes | Displaying UI | + /// + /// Use `get_my_trash_info` when you need to show trash items to the user with readable + /// names. Use `get_my_trash_sections` when you only need view IDs (e.g., checking if + /// a view is trashed). + /// + /// # Common Use Cases + /// + /// ## Display Trash Bin UI + /// ```rust,ignore + /// let trash_info = folder.get_my_trash_info(Some(current_user_id)); + /// for item in trash_info { + /// render_trash_item( + /// item.id, + /// &item.name, + /// format_timestamp(item.created_at) + /// ); + /// } + /// ``` + /// + /// ## Restore Deleted View by Name + /// ```rust,ignore + /// let trash = folder.get_my_trash_info(Some(uid)); + /// if let Some(item) = trash.iter().find(|t| t.name == "Important Doc") { + /// folder.delete_trash_view_ids(vec![item.id.to_string()], uid); + /// println!("Restored: {}", item.name); + /// } + /// ``` + /// + /// ## Show Time Since Deletion + /// ```rust,ignore + /// let trash = folder.get_my_trash_info(Some(uid)); + /// let now = current_timestamp(); + /// + /// for item in trash { + /// let days_ago = (now - item.created_at) / (24 * 60 * 60 * 1000); + /// println!("{} (deleted {} days ago)", item.name, days_ago); + /// } + /// ``` + /// + /// # Implementation Details + /// + /// Internally, this method: + /// 1. Calls `get_my_trash_sections(uid)` to get the list of trashed view IDs + /// 2. For each `SectionItem`, looks up the view name from the CRDT + /// 3. Combines ID + name + timestamp into `TrashInfo` + /// 4. Uses `flat_map` to filter out items where name lookup fails + /// + /// The operation requires two CRDT lookups per item (section + view name), so for + /// very large trash bins, `get_my_trash_sections` may be more efficient if you only + /// need IDs. + /// + /// # Related Methods + /// + /// - [`get_my_trash_sections`]: Get trash without view names (more efficient) + /// - [`add_trash_view_ids`]: Move views to trash (requires `i64`) + /// - [`delete_trash_view_ids`]: Permanently delete from trash (requires `i64`) + pub fn get_my_trash_info(&self, uid: Option) -> Vec { + let Some(uid_val) = uid else { + return vec![]; + }; let txn = self.collab.transact(); self - .get_my_trash_sections(uid) + .get_my_trash_sections(Some(uid_val)) .into_iter() .flat_map(|section| { self @@ -513,18 +1100,24 @@ impl Folder { pub fn replace_view(&mut self, from: &ViewId, to: &ViewId, uid: i64) -> bool { let mut txn = self.collab.transact_mut(); - self.body.replace_view(&mut txn, from, to, uid) + self.body.replace_view(&mut txn, from, to, Some(uid)) } - pub fn get_view(&self, view_id: &ViewId, uid: i64) -> Option> { + /// Get a view by id. When uid is provided, includes user-specific data like is_favorite. + /// When uid is None, returns base view data without user-specific enrichment. + pub fn get_view(&self, view_id: &ViewId, uid: Option) -> Option> { let txn = self.collab.transact(); self.body.views.get_view(&txn, view_id, uid) } - pub fn is_view_in_section(&self, section: Section, view_id: &ViewId, uid: i64) -> bool { + pub fn is_view_in_section(&self, section: Section, view_id: &ViewId, uid: Option) -> bool { let txn = self.collab.transact(); - if let Some(op) = self.body.section.section_op(&txn, section, uid) { - op.contains_with_txn(&txn, view_id) + if let Some(uid) = uid { + if let Some(op) = self.body.section.section_op(&txn, section, Some(uid)) { + op.contains_with_txn(&txn, view_id) + } else { + false + } } else { false } @@ -557,7 +1150,7 @@ impl Folder { /// # Returns /// /// * `Vec`: A vector of `View` objects that includes the parent view and all of its child views. - pub fn get_view_recursively(&self, view_id: &ViewId, uid: i64) -> Vec { + pub fn get_view_recursively(&self, view_id: &ViewId, uid: Option) -> Vec { let txn = self.collab.transact(); let mut views = vec![]; self.body.get_view_recursively_with_txn( @@ -697,13 +1290,14 @@ impl FolderBody { ); } - if let Some(fav_section) = section.section_op(&txn, Section::Favorite, folder_data.uid) { + if let Some(fav_section) = section.section_op(&txn, Section::Favorite, Some(folder_data.uid)) + { for (uid, sections) in folder_data.favorites { fav_section.add_sections_for_user_with_txn(&mut txn, &uid, sections); } } - if let Some(trash_section) = section.section_op(&txn, Section::Trash, folder_data.uid) { + if let Some(trash_section) = section.section_op(&txn, Section::Trash, Some(folder_data.uid)) { for (uid, sections) in folder_data.trash { trash_section.add_sections_for_user_with_txn(&mut txn, &uid, sections); } @@ -746,7 +1340,7 @@ impl FolderBody { view_id: &ViewId, visited: &mut HashSet, accumulated_views: &mut Vec, - uid: i64, + uid: Option, ) { let mut stack = vec![*view_id]; while let Some(current_id) = stack.pop() { @@ -766,7 +1360,7 @@ impl FolderBody { &self, txn: &T, workspace_id: &WorkspaceId, - uid: i64, + uid: Option, ) -> Option { let folder_workspace_id: String = self.meta.get_with_txn(txn, FOLDER_WORKSPACE_ID)?; // Convert workspace_id UUID to string for comparison @@ -784,8 +1378,9 @@ impl FolderBody { &self, txn: &T, workspace_id: &str, - uid: i64, + uid: Option, ) -> Option { + let uid = uid?; let folder_workspace_id = self.get_workspace_id_with_txn(txn)?; // Parse workspace_id as UUID, return None if invalid let workspace_uuid = match uuid::Uuid::parse_str(workspace_id) { @@ -806,25 +1401,28 @@ impl FolderBody { let workspace = Workspace::from( self .views - .get_view_with_txn(txn, &workspace_uuid, uid)? + .get_view_with_txn(txn, &workspace_uuid, Some(uid))? .as_ref(), ); - let current_view = self.get_current_view(txn, uid); + let current_view = self.get_current_view(txn, Some(uid)); let mut views = vec![]; let orphan_views = self .views - .get_orphan_views_with_txn(txn, uid) + .get_orphan_views_with_txn(txn, Some(uid)) .iter() .map(|view| view.as_ref().clone()) .collect::>(); - for view in self.views.get_views_belong_to(txn, &workspace_uuid, uid) { + for view in self + .views + .get_views_belong_to(txn, &workspace_uuid, Some(uid)) + { let mut all_views_in_workspace = vec![]; self.get_view_recursively_with_txn( txn, &view.id, &mut HashSet::default(), &mut all_views_in_workspace, - uid, + Some(uid), ); views.extend(all_views_in_workspace); } @@ -832,24 +1430,24 @@ impl FolderBody { let favorites = self .section - .section_op(txn, Section::Favorite, uid) + .section_op(txn, Section::Favorite, Some(uid)) .map(|op| op.get_sections(txn)) .unwrap_or_default(); let recent = self .section - .section_op(txn, Section::Recent, uid) + .section_op(txn, Section::Recent, Some(uid)) .map(|op| op.get_sections(txn)) .unwrap_or_default(); let trash = self .section - .section_op(txn, Section::Trash, uid) + .section_op(txn, Section::Trash, Some(uid)) .map(|op| op.get_sections(txn)) .unwrap_or_default(); let private = self .section - .section_op(txn, Section::Private, uid) + .section_op(txn, Section::Private, Some(uid)) .map(|op| op.get_sections(txn)) .unwrap_or_default(); @@ -869,7 +1467,7 @@ impl FolderBody { self.meta.get_with_txn(txn, FOLDER_WORKSPACE_ID) } - pub async fn observe_view_changes(&self, uid: i64) { + pub async fn observe_view_changes(&self, uid: Option) { self.views.observe_view_change(uid, HashMap::new()).await; } @@ -886,7 +1484,7 @@ impl FolderBody { view_id: &ViewId, from: u32, to: u32, - uid: i64, + uid: Option, ) -> Option> { let view = self.views.get_view_with_txn(txn, view_id, uid)?; if let Some(parent_uuid) = &view.parent_view_id { @@ -901,14 +1499,15 @@ impl FolderBody { view_id: &ViewId, new_parent_id: &ViewId, prev_view_id: Option, - uid: i64, + uid: Option, ) -> Option> { + let uid = uid?; tracing::debug!("Move nested view: {}", view_id); - let view = self.views.get_view_with_txn(txn, view_id, uid)?; + let view = self.views.get_view_with_txn(txn, view_id, Some(uid))?; let current_workspace_id = self.get_workspace_id_with_txn(txn)?; let parent_id = &view.parent_view_id; - let new_parent_view = self.views.get_view_with_txn(txn, new_parent_id, uid); + let new_parent_view = self.views.get_view_with_txn(txn, new_parent_id, Some(uid)); // If the new parent is not a view, it must be a workspace. // Check if the new parent is the current workspace, as moving out of the current workspace is not supported yet. @@ -938,17 +1537,18 @@ impl FolderBody { Some(view) } - pub fn get_child_of_first_public_view(&self, txn: &T, uid: i64) -> Option { + pub fn get_child_of_first_public_view( + &self, + txn: &T, + uid: Option, + ) -> Option { self .get_workspace_id(txn) - .and_then(|workspace_id| { - uuid::Uuid::parse_str(&workspace_id) - .ok() - .and_then(|uuid| self.views.get_view(txn, &uuid, uid)) - }) + .and_then(|workspace_id| uuid::Uuid::parse_str(&workspace_id).ok()) + .and_then(|uuid| self.views.get_view_with_txn(txn, &uuid, uid)) .and_then(|root_view| { let first_public_space_view_id_with_child = root_view.children.iter().find(|space_id| { - match self.views.get_view(txn, &space_id.id, uid) { + match self.views.get_view_with_txn(txn, &space_id.id, uid) { Some(space_view) => { let is_public_space = space_view .space_info() @@ -965,7 +1565,7 @@ impl FolderBody { .and_then(|first_public_space_view_id_with_child| { self .views - .get_view(txn, &first_public_space_view_id_with_child, uid) + .get_view_with_txn(txn, &first_public_space_view_id_with_child, uid) }) .and_then(|first_public_space_view_with_child| { first_public_space_view_with_child @@ -976,7 +1576,7 @@ impl FolderBody { }) } - pub fn get_current_view(&self, txn: &T, uid: i64) -> Option { + pub fn get_current_view(&self, txn: &T, uid: Option) -> Option { // Fallback to CURRENT_VIEW if CURRENT_VIEW_FOR_USER is not present. This could happen for // workspace folder created by older version of the app before CURRENT_VIEW_FOR_USER is introduced. // If user cannot be found in CURRENT_VIEW_FOR_USER, use the first child of the first public space @@ -985,24 +1585,32 @@ impl FolderBody { Some(YrsValue::YMap(map)) => Some(map), _ => None, }; - match current_view_for_user_map { - Some(current_view_for_user) => { + match (uid, current_view_for_user_map) { + (Some(uid), Some(current_view_for_user)) => { let view_for_user: Option = current_view_for_user.get_with_txn(txn, uid.to_string().as_ref()); view_for_user .and_then(|s| Uuid::parse_str(&s).ok()) - .or(self.get_child_of_first_public_view(txn, uid)) + .or_else(|| self.get_child_of_first_public_view(txn, Some(uid))) }, - None => { + (Some(uid), None) => { + let current_view: Option = self.meta.get_with_txn(txn, CURRENT_VIEW); + current_view + .and_then(|s| Uuid::parse_str(&s).ok()) + .or_else(|| self.get_child_of_first_public_view(txn, Some(uid))) + }, + (None, _) => { let current_view: Option = self.meta.get_with_txn(txn, CURRENT_VIEW); current_view.and_then(|s| Uuid::parse_str(&s).ok()) }, } } - pub fn set_current_view(&self, txn: &mut TransactionMut, view: ViewId, uid: i64) { - let current_view_for_user = self.meta.get_or_init_map(txn, CURRENT_VIEW_FOR_USER); - current_view_for_user.try_update(txn, uid.to_string(), view.to_string()); + pub fn set_current_view(&self, txn: &mut TransactionMut, view: ViewId, uid: Option) { + if let Some(uid) = uid { + let current_view_for_user = self.meta.get_or_init_map(txn, CURRENT_VIEW_FOR_USER); + current_view_for_user.try_update(txn, uid.to_string(), view.to_string()); + } } pub fn replace_view( @@ -1010,9 +1618,11 @@ impl FolderBody { txn: &mut TransactionMut, old_view_id: &ViewId, new_view_id: &ViewId, - uid: i64, + uid: Option, ) -> bool { - self.views.replace_view(txn, old_view_id, new_view_id, uid) + uid + .map(|uid| self.views.replace_view(txn, old_view_id, new_view_id, uid)) + .unwrap_or(false) } } @@ -1195,7 +1805,7 @@ mod tests { private: Default::default(), }; let mut folder = Folder::create(collab, None, folder_data); - let favorite_sections = folder.get_all_favorites_sections(uid); + let favorite_sections = folder.get_all_favorites_sections(Some(uid)); // Initially, all 3 views should be in favorites assert_eq!(favorite_sections.len(), 3); assert_eq!(favorite_sections[0].id, views[0].id); @@ -1207,7 +1817,7 @@ mod tests { Some(&views[1].id.to_string()), uid, ); - let favorite_sections = folder.get_all_favorites_sections(uid); + let favorite_sections = folder.get_all_favorites_sections(Some(uid)); // After moving views[0] after views[1], order should be: views[1], views[0], views[2] assert_eq!(favorite_sections.len(), 3); assert_eq!(favorite_sections[0].id, views[1].id); @@ -1215,7 +1825,7 @@ mod tests { assert_eq!(favorite_sections[2].id, views[2].id); // Move views[2] to the beginning (None means first position) folder.move_favorite_view_id(&views[2].id.to_string(), None, uid); - let favorite_sections = folder.get_all_favorites_sections(uid); + let favorite_sections = folder.get_all_favorites_sections(Some(uid)); // After moving views[2] to the beginning, order should be: views[2], views[1], views[0] assert_eq!(favorite_sections.len(), 3); assert_eq!(favorite_sections[0].id, views[2].id); diff --git a/collab/src/folder/folder_observe.rs b/collab/src/folder/folder_observe.rs index ab1b19496..f7bf31b44 100644 --- a/collab/src/folder/folder_observe.rs +++ b/collab/src/folder/folder_observe.rs @@ -52,7 +52,7 @@ pub(crate) fn subscribe_view_change( txn, &view_relations, §ion_map, - uid, + Some(uid), mappings, ) { deletion_cache.insert(view.id, Arc::new(view.clone())); @@ -78,7 +78,7 @@ pub(crate) fn subscribe_view_change( txn, &view_relations, §ion_map, - uid, + Some(uid), mappings, ) { // Update deletion cache with the updated view diff --git a/collab/src/folder/section.rs b/collab/src/folder/section.rs index 30ae5d4cb..106e7a14e 100644 --- a/collab/src/folder/section.rs +++ b/collab/src/folder/section.rs @@ -45,11 +45,11 @@ impl SectionMap { &self, txn: &T, section: Section, - uid: i64, + uid: Option, ) -> Option { let container = self.get_section(txn, section.as_ref())?; Some(SectionOperation { - uid: UserId::from(uid), + uid: uid.map(UserId::from), container, section, change_tx: self.change_tx.clone(), @@ -65,6 +65,71 @@ impl SectionMap { } } +/// Represents different types of user-specific view collections in a folder. +/// +/// Sections are **per-user** organizational categories that allow each user in a +/// collaborative folder to maintain their own personal view collections. Each section +/// type has a specific semantic purpose. +/// +/// # Section Types +/// +/// ## Predefined Sections +/// +/// - **Favorite**: Views the user has marked as favorites for quick access +/// - **Recent**: Recently accessed views, typically ordered by access time +/// - **Trash**: Views the user has deleted (pending permanent removal) +/// - **Private**: Views that are private to the user and hidden from others +/// +/// ## Custom Sections +/// +/// - **Custom(String)**: User-defined section types for extensibility +/// +/// # Storage Architecture +/// +/// Each section in the CRDT is stored as a nested map structure: +/// +/// ```text +/// SectionMap +/// ├─ "favorite" (MapRef) +/// │ ├─ "1" (uid) → Array[SectionItem, SectionItem, ...] +/// │ ├─ "2" (uid) → Array[SectionItem, SectionItem, ...] +/// │ └─ ... +/// ├─ "recent" (MapRef) +/// │ └─ ... +/// ├─ "trash" (MapRef) +/// │ └─ ... +/// └─ "private" (MapRef) +/// └─ ... +/// ``` +/// +/// This allows multiple users to collaborate on the same folder while maintaining +/// independent personal collections. For example: +/// - User 1's favorites don't affect User 2's favorites +/// - Each user has their own trash bin +/// - Each user has their own private views +/// +/// # String Representation +/// +/// Each section variant has a unique string identifier used as the CRDT map key: +/// - `Favorite` → `"favorite"` +/// - `Recent` → `"recent"` +/// - `Trash` → `"trash"` +/// - `Private` → `"private"` +/// - `Custom("my_section")` → `"my_section"` +/// +/// # Examples +/// +/// ```rust,ignore +/// use collab::folder::Section; +/// +/// // Predefined sections +/// let fav = Section::Favorite; +/// assert_eq!(fav.as_ref(), "favorite"); +/// +/// // Custom section +/// let custom = Section::from("my_custom_section".to_string()); +/// assert_eq!(custom.as_ref(), "my_custom_section"); +/// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub enum Section { Favorite, @@ -119,7 +184,7 @@ pub enum TrashSectionChange { pub type SectionsByUid = HashMap>; pub struct SectionOperation { - uid: UserId, + uid: Option, container: MapRef, section: Section, change_tx: Option, @@ -130,8 +195,8 @@ impl SectionOperation { &self.container } - fn uid(&self) -> &UserId { - &self.uid + fn uid(&self) -> Option<&UserId> { + self.uid.as_ref() } pub fn get_sections(&self, txn: &T) -> SectionsByUid { @@ -154,9 +219,12 @@ impl SectionOperation { } pub fn contains_with_txn(&self, txn: &T, view_id: &ViewId) -> bool { + let Some(uid) = self.uid() else { + return false; + }; match self .container() - .get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref()) + .get_with_txn::<_, ArrayRef>(txn, uid.as_ref()) { None => false, Some(array) => { @@ -173,9 +241,12 @@ impl SectionOperation { } pub fn get_all_section_item(&self, txn: &T) -> Vec { + let Some(uid) = self.uid() else { + return vec![]; + }; match self .container() - .get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref()) + .get_with_txn::<_, ArrayRef>(txn, uid.as_ref()) { None => vec![], Some(array) => { @@ -201,6 +272,9 @@ impl SectionOperation { id: T, prev_id: Option, ) { + let Some(uid) = self.uid() else { + return; + }; let section_items = self.get_all_section_item(txn); let id = id.as_ref(); let old_pos = section_items @@ -217,7 +291,7 @@ impl SectionOperation { .unwrap_or(0); let section_array = self .container() - .get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref()); + .get_with_txn::<_, ArrayRef>(txn, uid.as_ref()); // If the new position index is greater than the length of the section, yrs will panic if new_pos > section_items.len() as u32 { return; @@ -233,9 +307,12 @@ impl SectionOperation { txn: &mut TransactionMut, ids: Vec, ) { + let Some(uid) = self.uid() else { + return; + }; if let Some(fav_array) = self .container() - .get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref()) + .get_with_txn::<_, ArrayRef>(txn, uid.as_ref()) { for id in &ids { if let Some(pos) = self @@ -267,8 +344,11 @@ impl SectionOperation { } pub fn add_sections_item(&self, txn: &mut TransactionMut, items: Vec) { + let Some(uid) = self.uid() else { + return; + }; let item_ids = items.iter().map(|item| item.id).collect::>(); - self.add_sections_for_user_with_txn(txn, self.uid(), items); + self.add_sections_for_user_with_txn(txn, uid, items); if let Some(change_tx) = self.change_tx.as_ref() { match self.section { Section::Favorite => {}, @@ -298,9 +378,12 @@ impl SectionOperation { } pub fn clear(&self, txn: &mut TransactionMut) { + let Some(uid) = self.uid() else { + return; + }; if let Some(array) = self .container() - .get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref()) + .get_with_txn::<_, ArrayRef>(txn, uid.as_ref()) { let len = array.iter(txn).count(); array.remove_range(txn, 0, len as u32); @@ -308,6 +391,64 @@ impl SectionOperation { } } +/// An item in a user's section, representing a view and when it was added. +/// +/// `SectionItem` is the fundamental unit stored in sections (Favorite, Recent, Trash, Private). +/// Each item records both which view is in the section and when it was added, enabling +/// time-based operations like sorting by recency or tracking deletion times. +/// +/// # Fields +/// +/// * `id` - The UUID of the view in this section +/// * `timestamp` - Unix timestamp (milliseconds) when the view was added to this section +/// +/// # Storage Format +/// +/// SectionItems are serialized as Yrs `Any` values (essentially JSON-like maps) in the CRDT: +/// +/// ```json +/// { +/// "id": "550e8400-e29b-41d4-a716-446655440000", +/// "timestamp": 1704067200000 +/// } +/// ``` +/// +/// Multiple items are stored in a Yrs array per user per section: +/// +/// ```text +/// section["favorite"]["123"] = [ +/// SectionItem { id: view_1, timestamp: 1704067200000 }, +/// SectionItem { id: view_2, timestamp: 1704068000000 }, +/// ... +/// ] +/// ``` +/// +/// # Usage Patterns +/// +/// ## Favorite Sections +/// ```rust,ignore +/// let favorites = folder.get_my_favorite_sections(Some(uid)); +/// for item in favorites { +/// println!("View {} favorited at {}", item.id, item.timestamp); +/// } +/// ``` +/// +/// ## Recent Sections (sorted by timestamp) +/// ```rust,ignore +/// let mut recent = folder.get_my_recent_sections(Some(uid)); +/// recent.sort_by_key(|item| std::cmp::Reverse(item.timestamp)); // newest first +/// let most_recent = recent.first(); +/// ``` +/// +/// ## Trash Sections (check deletion time) +/// ```rust,ignore +/// let trash = folder.get_my_trash_sections(Some(uid)); +/// let thirty_days_ago = current_timestamp() - (30 * 24 * 60 * 60 * 1000); +/// let permanently_delete = trash +/// .iter() +/// .filter(|item| item.timestamp < thirty_days_ago) +/// .collect::>(); +/// ``` #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct SectionItem { pub id: ViewId, diff --git a/collab/src/folder/view.rs b/collab/src/folder/view.rs index aa90ebbd0..e45a63664 100644 --- a/collab/src/folder/view.rs +++ b/collab/src/folder/view.rs @@ -72,7 +72,11 @@ impl ViewsMap { } } - pub async fn observe_view_change(&self, uid: i64, views: HashMap>) { + /// Observe view changes for a specific user. Requires uid to properly track user-specific changes. + pub async fn observe_view_change(&self, uid: Option, views: HashMap>) { + let Some(uid) = uid else { + return; // Cannot observe changes without uid + }; for (k, v) in views { self.deletion_cache.insert(k, v); } @@ -157,11 +161,12 @@ impl ViewsMap { } } + /// Get views belonging to a parent. When uid is provided, includes user-specific enrichment. pub fn get_views_belong_to( &self, txn: &T, parent_view_id: &ViewId, - uid: i64, + uid: Option, ) -> Vec> { match self.get_view_with_txn(txn, parent_view_id, uid) { Some(root_view) => root_view @@ -205,14 +210,21 @@ impl ViewsMap { } } - pub fn get_views(&self, txn: &T, view_ids: &[ViewId], uid: i64) -> Vec> { + /// Get multiple views by ids. When uid is provided, includes user-specific enrichment. + pub fn get_views( + &self, + txn: &T, + view_ids: &[ViewId], + uid: Option, + ) -> Vec> { view_ids .iter() .flat_map(|view_id| self.get_view_with_txn(txn, view_id, uid)) .collect::>() } - pub fn get_all_views(&self, txn: &T, uid: i64) -> Vec> { + /// Get all views. When uid is provided, includes user-specific enrichment like is_favorite. + pub fn get_all_views(&self, txn: &T, uid: Option) -> Vec> { // since views can be mapped through revisions, we need a map of final_view_id and its predecessors // first split the keys into ones that have existing mappings (roots) and ones that have not more mapping (leafs) @@ -252,14 +264,21 @@ impl ViewsMap { .collect() } + /// Get a view by id. When uid is provided, includes user-specific enrichment like is_favorite. #[instrument(level = "trace", skip_all)] - pub fn get_view(&self, txn: &T, view_id: &ViewId, uid: i64) -> Option> { + pub fn get_view( + &self, + txn: &T, + view_id: &ViewId, + uid: Option, + ) -> Option> { self.get_view_with_txn(txn, view_id, uid) } - /// Return the orphan views.d + /// Return the orphan views. /// The orphan views are the views that its parent_view_id equal to its view_id. - pub fn get_orphan_views_with_txn(&self, txn: &T, uid: i64) -> Vec> { + /// When uid is provided, includes user-specific enrichment. + pub fn get_orphan_views_with_txn(&self, txn: &T, uid: Option) -> Vec> { self .container .keys(txn) @@ -279,7 +298,7 @@ impl ViewsMap { &self, txn: &T, view_id: &ViewId, - uid: i64, + uid: Option, ) -> Option> { let (view_id, mappings) = self.revision_map.mappings(txn, *view_id); let map_ref = self.container.get_with_txn(txn, &view_id.to_string())?; @@ -297,11 +316,12 @@ impl ViewsMap { /// Gets a view with stronger consistency guarantees, bypassing cache when needed /// Use this during transactions that might have uncommitted changes /// Note: Since we removed the cache, this is now identical to get_view_with_txn + /// When uid is provided, includes user-specific enrichment like is_favorite. pub fn get_view_with_strong_consistency( &self, txn: &T, view_id: &ViewId, - uid: i64, + uid: Option, ) -> Option> { self.get_view_with_txn(txn, view_id, uid) } @@ -493,7 +513,7 @@ impl ViewsMap { new_view_id: &ViewId, uid: i64, ) -> bool { - if let Some(old_view) = self.get_view(txn, old_view_id, uid) { + if let Some(old_view) = self.get_view(txn, old_view_id, Some(uid)) { let mut new_view = (*old_view).clone(); new_view.id = *new_view_id; new_view.last_edited_by = Some(uid); @@ -517,7 +537,7 @@ pub(crate) fn view_from_map_ref( txn: &T, view_relations: &Arc, section_map: &SectionMap, - uid: i64, + uid: Option, mappings: impl IntoIterator, ) -> Option { let parent_view_id: String = map_ref.get_with_txn(txn, VIEW_PARENT_ID)?; @@ -556,9 +576,12 @@ pub(crate) fn view_from_map_ref( } let icon = get_icon_from_view_map(map_ref, txn); - let is_favorite = section_map - .section_op(txn, Section::Favorite, uid) - .map(|op| op.contains_with_txn(txn, &id)) + let is_favorite = uid + .and_then(|uid| { + section_map + .section_op(txn, Section::Favorite, Some(uid)) + .map(|op| op.contains_with_txn(txn, &id)) + }) .unwrap_or(false); let created_by = map_ref.get_with_txn(txn, VIEW_CREATED_BY); @@ -821,7 +844,7 @@ impl<'a, 'b, 'c> ViewUpdate<'a, 'b, 'c> { if let Some(private_section) = self .section_map - .section_op(self.txn, Section::Private, self.uid.as_i64()) + .section_op(self.txn, Section::Private, Some(self.uid.as_i64())) { if is_private { private_section.add_sections_item(self.txn, vec![SectionItem::new(*self.view_id)]); @@ -837,7 +860,7 @@ impl<'a, 'b, 'c> ViewUpdate<'a, 'b, 'c> { if let Some(fav_section) = self .section_map - .section_op(self.txn, Section::Favorite, self.uid.as_i64()) + .section_op(self.txn, Section::Favorite, Some(self.uid.as_i64())) { if is_favorite { fav_section.add_sections_item(self.txn, vec![SectionItem::new(*self.view_id)]); @@ -861,7 +884,7 @@ impl<'a, 'b, 'c> ViewUpdate<'a, 'b, 'c> { if let Some(trash_section) = self .section_map - .section_op(self.txn, Section::Trash, self.uid.as_i64()) + .section_op(self.txn, Section::Trash, Some(self.uid.as_i64())) { if is_trash { trash_section.add_sections_item(self.txn, vec![SectionItem::new(*self.view_id)]); @@ -891,7 +914,7 @@ impl<'a, 'b, 'c> ViewUpdate<'a, 'b, 'c> { self.txn, &self.children_map, self.section_map, - self.uid.as_i64(), + Some(self.uid.as_i64()), [], ) } diff --git a/collab/tests/document/conversions/plain_text_test.rs b/collab/tests/document/conversions/plain_text_test.rs index ed8e8952d..58118e6f9 100644 --- a/collab/tests/document/conversions/plain_text_test.rs +++ b/collab/tests/document/conversions/plain_text_test.rs @@ -139,7 +139,7 @@ fn plain_text_mentions_custom_resolver() { let options = PlainTextExportOptions::from_default_resolver(resolver); let plain = document - .to_plain_text_with_options(options) + .to_plain_text_with_options(&options) .into_iter() .filter(|line| !line.trim().is_empty()) .collect::>(); diff --git a/collab/tests/folder/child_views_test.rs b/collab/tests/folder/child_views_test.rs index 13a29da0f..584a29449 100644 --- a/collab/tests/folder/child_views_test.rs +++ b/collab/tests/folder/child_views_test.rs @@ -48,19 +48,20 @@ fn create_child_views_test() { let v_1_child_views = folder .body .views - .get_views_belong_to(&txn, &v_1.id, uid.as_i64()); + .get_views_belong_to(&txn, &v_1.id, Some(uid.as_i64())); assert_eq!(v_1_child_views.len(), 3); - let v_1_2_child_views = folder - .body - .views - .get_views_belong_to(&txn, &v_1_2.id, uid.as_i64()); + let v_1_2_child_views = + folder + .body + .views + .get_views_belong_to(&txn, &v_1_2.id, Some(uid.as_i64())); assert_eq!(v_1_2_child_views.len(), 2); let workspace_uuid_str = workspace_id.to_string(); let folder_data = folder .body - .get_folder_data(&txn, &workspace_uuid_str, uid.as_i64()) + .get_folder_data(&txn, &workspace_uuid_str, Some(uid.as_i64())) .unwrap(); let value = serde_json::to_value(folder_data).unwrap(); let fake_w_1_uuid = workspace_uuid_str.clone(); @@ -217,7 +218,7 @@ fn move_child_views_test() { let v_1_child_views = folder .body .views - .get_views_belong_to(&txn, &v_1.id, uid.as_i64()); + .get_views_belong_to(&txn, &v_1.id, Some(uid.as_i64())); assert_eq!( v_1_child_views[0].id.to_string(), uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "1_1".as_bytes()).to_string() @@ -237,7 +238,7 @@ fn move_child_views_test() { let v_1_child_views = folder .body .views - .get_view(&txn, &v_1.id, uid.as_i64()) + .get_view(&txn, &v_1.id, Some(uid.as_i64())) .unwrap(); assert_eq!( v_1_child_views.children[0].id.to_string(), @@ -280,10 +281,11 @@ fn delete_view_test() { .insert(&mut txn, view_3, None, uid.as_i64()); folder.body.views.remove_child(&mut txn, &workspace_id, 1); - let w_1_child_views = folder - .body - .views - .get_views_belong_to(&txn, &workspace_id, uid.as_i64()); + let w_1_child_views = + folder + .body + .views + .get_views_belong_to(&txn, &workspace_id, Some(uid.as_i64())); assert_eq!( w_1_child_views[0].id.to_string(), uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "1_1".as_bytes()).to_string() @@ -321,17 +323,19 @@ fn delete_child_view_test() { .views .insert(&mut txn, view_2, None, uid.as_i64()); - let views = folder - .body - .views - .get_views_belong_to(&txn, &parse_view_id(&view_1_id), uid.as_i64()); + let views = + folder + .body + .views + .get_views_belong_to(&txn, &parse_view_id(&view_1_id), Some(uid.as_i64())); assert_eq!(views.len(), 1); folder.body.views.delete_views(&mut txn, vec![view_1_1_id]); - let views = folder - .body - .views - .get_views_belong_to(&txn, &parse_view_id(&view_1_id), uid.as_i64()); + let views = + folder + .body + .views + .get_views_belong_to(&txn, &parse_view_id(&view_1_id), Some(uid.as_i64())); assert!(views.is_empty()); } @@ -361,19 +365,19 @@ fn create_orphan_child_views_test() { let child_views = folder .body .views - .get_views_belong_to(&txn, &workspace_id, uid.as_i64()); + .get_views_belong_to(&txn, &workspace_id, Some(uid.as_i64())); assert_eq!(child_views.len(), 1); let orphan_views = folder .body .views - .get_orphan_views_with_txn(&txn, uid.as_i64()); + .get_orphan_views_with_txn(&txn, Some(uid.as_i64())); assert_eq!(orphan_views.len(), 1); // The folder data should contains the orphan view let folder_data = folder .body - .get_folder_data(&txn, &workspace_uuid_str, uid.as_i64()) + .get_folder_data(&txn, &workspace_uuid_str, Some(uid.as_i64())) .unwrap(); let fake_w_1_uuid = workspace_uuid_str.clone(); let id_1_uuid = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "1".as_bytes()).to_string(); diff --git a/collab/tests/folder/custom_section.rs b/collab/tests/folder/custom_section.rs index 5f54ec16c..68e487a99 100644 --- a/collab/tests/folder/custom_section.rs +++ b/collab/tests/folder/custom_section.rs @@ -19,7 +19,7 @@ fn custom_section_test() { let op = folder .body .section - .section_op(&txn, Section::Favorite, uid.as_i64()) + .section_op(&txn, Section::Favorite, Some(uid.as_i64())) .unwrap(); op.add_sections_item( &mut txn, @@ -33,7 +33,11 @@ fn custom_section_test() { let op = folder .body .section - .section_op(&txn, Section::Custom("private".to_string()), uid.as_i64()) + .section_op( + &txn, + Section::Custom("private".to_string()), + Some(uid.as_i64()), + ) .unwrap(); op.add_sections_item( &mut txn, diff --git a/collab/tests/folder/favorite_test.rs b/collab/tests/folder/favorite_test.rs index fd96d318e..d778928b3 100644 --- a/collab/tests/folder/favorite_test.rs +++ b/collab/tests/folder/favorite_test.rs @@ -21,14 +21,14 @@ fn create_favorite_test() { // Get view_1 from folder let view_1 = folder - .get_view(&parse_view_id(&view_1_id), uid.as_i64()) + .get_view(&parse_view_id(&view_1_id), Some(uid.as_i64())) .unwrap(); assert!(!view_1.is_favorite); folder.add_favorite_view_ids(vec![view_1_id.clone()], uid.as_i64()); // Check if view_1 is favorite let view_1 = folder - .get_view(&parse_view_id(&view_1_id), uid.as_i64()) + .get_view(&parse_view_id(&view_1_id), Some(uid.as_i64())) .unwrap(); assert!(view_1.is_favorite); @@ -36,11 +36,11 @@ fn create_favorite_test() { let view_2 = make_test_view("2", workspace_id, vec![]); folder.insert_view(view_2, None, uid.as_i64()); - let views = - folder - .body - .views - .get_views_belong_to(&folder.collab.transact(), &workspace_id, uid.as_i64()); + let views = folder.body.views.get_views_belong_to( + &folder.collab.transact(), + &workspace_id, + Some(uid.as_i64()), + ); assert_eq!(views.len(), 2); assert_eq!( views[0].id.to_string(), @@ -54,7 +54,7 @@ fn create_favorite_test() { ); assert!(!views[1].is_favorite); - let favorites = folder.get_my_favorite_sections(uid.as_i64()); + let favorites = folder.get_my_favorite_sections(Some(uid.as_i64())); assert_eq!(favorites.len(), 1); } @@ -76,7 +76,7 @@ fn add_favorite_view_and_then_remove_test() { folder .body .views - .get_views_belong_to(&folder.transact(), &workspace_id, uid.as_i64()); + .get_views_belong_to(&folder.transact(), &workspace_id, Some(uid.as_i64())); assert_eq!(views.len(), 1); assert_eq!( views[0].id.to_string(), @@ -89,7 +89,7 @@ fn add_favorite_view_and_then_remove_test() { folder .body .views - .get_views_belong_to(&folder.transact(), &workspace_id, uid.as_i64()); + .get_views_belong_to(&folder.transact(), &workspace_id, Some(uid.as_i64())); assert!(!views[0].is_favorite); } @@ -112,7 +112,7 @@ fn create_multiple_user_favorite_test() { folder_1.insert_view(view_2, None, uid_1.as_i64()); folder_1.add_favorite_view_ids(vec![view_1_id.clone(), view_2_id.clone()], uid_1.as_i64()); - let favorites = folder_1.get_my_favorite_sections(uid_1.as_i64()); + let favorites = folder_1.get_my_favorite_sections(Some(uid_1.as_i64())); assert_eq!(favorites.len(), 2); assert_eq!( favorites[0].id.to_string(), @@ -124,13 +124,13 @@ fn create_multiple_user_favorite_test() { ); let workspace_uuid_str = workspace_id.to_string(); let folder_data = folder_1 - .get_folder_data(&workspace_uuid_str, uid_1.as_i64()) + .get_folder_data(&workspace_uuid_str, Some(uid_1.as_i64())) .unwrap(); let uid_2 = UserId::from(2); let folder_test2 = create_folder_with_data(uid_2.clone(), view_id_from_any_string("w1"), folder_data); - let favorites = folder_test2.get_my_favorite_sections(uid_2.as_i64()); + let favorites = folder_test2.get_my_favorite_sections(Some(uid_2.as_i64())); // User 2 can't see user 1's favorites assert!(favorites.is_empty()); @@ -157,7 +157,7 @@ fn favorite_data_serde_test() { folder.add_favorite_view_ids(vec![view_1_id, view_2_id], uid_1.as_i64()); let workspace_uuid_str = workspace_id.to_string(); let folder_data = folder - .get_folder_data(&workspace_uuid_str, uid_1.as_i64()) + .get_folder_data(&workspace_uuid_str, Some(uid_1.as_i64())) .unwrap(); let value = serde_json::to_value(&folder_data).unwrap(); let w1_uuid = workspace_uuid_str.clone(); @@ -215,7 +215,7 @@ fn delete_favorite_test() { // Add favorites folder.add_favorite_view_ids(vec![view_1_id.clone(), view_2_id], uid.as_i64()); - let favorites = folder.get_my_favorite_sections(uid.as_i64()); + let favorites = folder.get_my_favorite_sections(Some(uid.as_i64())); assert_eq!(favorites.len(), 2); assert_eq!( favorites[0].id.to_string(), @@ -227,7 +227,7 @@ fn delete_favorite_test() { ); folder.delete_favorite_view_ids(vec![view_1_id], uid.as_i64()); - let favorites = folder.get_my_favorite_sections(uid.as_i64()); + let favorites = folder.get_my_favorite_sections(Some(uid.as_i64())); assert_eq!(favorites.len(), 1); assert_eq!( favorites[0].id.to_string(), @@ -235,6 +235,6 @@ fn delete_favorite_test() { ); folder.remove_all_my_favorite_sections(uid.as_i64()); - let favorites = folder.get_my_favorite_sections(uid.as_i64()); + let favorites = folder.get_my_favorite_sections(Some(uid.as_i64())); assert_eq!(favorites.len(), 0); } diff --git a/collab/tests/folder/recent_views_test.rs b/collab/tests/folder/recent_views_test.rs index 96fa6895a..d36eab3a2 100644 --- a/collab/tests/folder/recent_views_test.rs +++ b/collab/tests/folder/recent_views_test.rs @@ -21,11 +21,11 @@ fn create_recent_views_test() { // Get view_1 from folder let view_1 = folder.get_view(id_1, uid.as_i64()).unwrap(); // Check if view_1 has been added into recent section. - assert!(!folder.is_view_in_section(Section::Recent, &view_1.id, uid.as_i64())); + assert!(!folder.is_view_in_section(Section::Recent, &view_1.id, Some(uid.as_i64()))); folder.add_recent_view_ids(vec![id_1.to_string()], uid.as_i64()); let view_1 = folder.get_view(id_1, uid.as_i64()).unwrap(); - assert!(folder.is_view_in_section(Section::Recent, &view_1.id, uid.as_i64())); + assert!(folder.is_view_in_section(Section::Recent, &view_1.id, Some(uid.as_i64()))); let id_2: &str = "view_2"; @@ -69,7 +69,7 @@ fn add_view_into_recent_and_then_remove_it_test() { assert_eq!(views.len(), 1); assert_eq!(views[0].id, id_1); // in recent section - assert!(folder.is_view_in_section(Section::Recent, &views[0].id, uid.as_i64())); + assert!(folder.is_view_in_section(Section::Recent, &views[0].id, Some(uid.as_i64()))); folder.delete_recent_view_ids(vec![id_1.to_string()], uid.as_i64()); let views = diff --git a/collab/tests/folder/replace_view_test.rs b/collab/tests/folder/replace_view_test.rs index 2e0e80a43..5013baf3f 100644 --- a/collab/tests/folder/replace_view_test.rs +++ b/collab/tests/folder/replace_view_test.rs @@ -29,7 +29,7 @@ fn replace_view_get_view() { folder.insert_view(v2, None, uid); let v2_id = crate::util::test_uuid("v2").to_string(); - let old = folder.get_view(&parse_view_id(&v2_id), uid).unwrap(); + let old = folder.get_view(&parse_view_id(&v2_id), Some(uid)).unwrap(); assert_eq!( old.id.to_string(), uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "v2".as_bytes()).to_string() @@ -42,7 +42,7 @@ fn replace_view_get_view() { ); // getting old view id should return new one - let new = folder.get_view(&parse_view_id(&v2_id), uid).unwrap(); + let new = folder.get_view(&parse_view_id(&v2_id), Some(uid)).unwrap(); assert_eq!( new.id.to_string(), uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "v3".as_bytes()).to_string() @@ -108,7 +108,7 @@ fn replace_view_get_view_concurrent_update() { .unwrap(); let v2_id = crate::util::test_uuid("v2").to_string(); - let v1 = f1.get_view(&parse_view_id(&v2_id), uid2).unwrap(); + let v1 = f1.get_view(&parse_view_id(&v2_id), Some(uid2)).unwrap(); assert_eq!( v1.id.to_string(), uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "v3".as_bytes()).to_string() @@ -125,7 +125,7 @@ fn replace_view_get_view_concurrent_update() { ] ); - let v2 = f2.get_view(&parse_view_id(&v2_id), uid1).unwrap(); + let v2 = f2.get_view(&parse_view_id(&v2_id), Some(uid1)).unwrap(); assert_eq!(v1, v2); } @@ -185,8 +185,8 @@ fn replace_view_all_views_concurrent_update() { // check if both sides have the same views assert_eq!(f1.to_json_value(), f2.to_json_value()); - let mut v1 = f1.get_all_views(uid1); - let mut v2 = f2.get_all_views(uid2); + let mut v1 = f1.get_all_views(Some(uid1)); + let mut v2 = f2.get_all_views(Some(uid2)); v1.sort_by_key(|v| v.id.to_string()); v2.sort_by_key(|v| v.id.to_string()); diff --git a/collab/tests/folder/serde_test.rs b/collab/tests/folder/serde_test.rs index 3a54d0e51..b90ec1d28 100644 --- a/collab/tests/folder/serde_test.rs +++ b/collab/tests/folder/serde_test.rs @@ -67,7 +67,7 @@ fn view_json_serde() { let views = folder .body .views - .get_views_belong_to(&txn, &workspace_id, uid.as_i64()); + .get_views_belong_to(&txn, &workspace_id, Some(uid.as_i64())); assert_eq!(views.len(), 2); } @@ -256,14 +256,14 @@ async fn deserialize_folder_data() { let handle = tokio::spawn(async move { let start = Instant::now(); let _trash_ids = folder - .get_all_trash_sections(clone_uid.as_i64()) + .get_all_trash_sections(Some(clone_uid.as_i64())) .into_iter() .map(|trash| trash.id) .collect::>(); // get the private view ids let _private_view_ids = folder - .get_all_private_sections(clone_uid.as_i64()) + .get_all_private_sections(Some(clone_uid.as_i64())) .into_iter() .map(|view| view.id) .collect::>(); @@ -290,13 +290,13 @@ fn get_view_ids_should_be_filtered(folder: &Folder, uid: i64) -> Vec { fn get_other_private_view_ids(folder: &Folder, uid: i64) -> Vec { let my_private_view_ids = folder - .get_my_private_sections(uid) + .get_my_private_sections(Some(uid)) .into_iter() .map(|view| view.id) .collect::>(); let all_private_view_ids = folder - .get_all_private_sections(uid) + .get_all_private_sections(Some(uid)) .into_iter() .map(|view| view.id) .collect::>(); @@ -309,7 +309,7 @@ fn get_other_private_view_ids(folder: &Folder, uid: i64) -> Vec { fn get_all_trash_ids(folder: &Folder, uid: i64) -> Vec { let trash_ids = folder - .get_all_trash_sections(uid) + .get_all_trash_sections(Some(uid)) .into_iter() .map(|trash| trash.id) .collect::>(); @@ -335,7 +335,7 @@ fn get_all_child_view_ids( let child_views = folder .body .views - .get_views_belong_to(txn, &parse_view_id(view_id), uid); + .get_views_belong_to(txn, &parse_view_id(view_id), Some(uid)); let child_view_ids = child_views .iter() .map(|view| view.id) diff --git a/collab/tests/folder/trash_test.rs b/collab/tests/folder/trash_test.rs index f29ac0542..d92b7a878 100644 --- a/collab/tests/folder/trash_test.rs +++ b/collab/tests/folder/trash_test.rs @@ -24,7 +24,7 @@ fn create_trash_test() { folder.add_trash_view_ids(vec![view_1_id, view_2_id, view_3_id], uid.as_i64()); - let trash = folder.get_my_trash_sections(uid.as_i64()); + let trash = folder.get_my_trash_sections(Some(uid.as_i64())); assert_eq!(trash.len(), 3); assert_eq!( trash[0].id.to_string(), @@ -56,7 +56,7 @@ fn delete_trash_view_ids_test() { folder.add_trash_view_ids(vec![view_1_id.clone(), view_2_id], uid.as_i64()); - let trash = folder.get_my_trash_sections(uid.as_i64()); + let trash = folder.get_my_trash_sections(Some(uid.as_i64())); assert_eq!( trash[0].id.to_string(), uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "v1".as_bytes()).to_string() @@ -67,7 +67,7 @@ fn delete_trash_view_ids_test() { ); folder.delete_trash_view_ids(vec![view_1_id], uid.as_i64()); - let trash = folder.get_my_trash_sections(uid.as_i64()); + let trash = folder.get_my_trash_sections(Some(uid.as_i64())); assert_eq!( trash[0].id.to_string(), uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, "v2".as_bytes()).to_string() diff --git a/collab/tests/folder/view_test.rs b/collab/tests/folder/view_test.rs index 4fa1fa55b..483ae7259 100644 --- a/collab/tests/folder/view_test.rs +++ b/collab/tests/folder/view_test.rs @@ -23,7 +23,7 @@ fn create_view_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(o_view.name, r_view.name); assert_eq!(o_view.parent_view_id, r_view.parent_view_id); @@ -53,7 +53,7 @@ fn create_view_with_sub_view_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(view.name, r_view.name); assert_eq!(view.parent_view_id, r_view.parent_view_id); @@ -62,12 +62,12 @@ fn create_view_with_sub_view_test() { let r_sub_view = folder .body .views - .get_view(&txn, &r_view.children[0].id, uid.as_i64()) + .get_view(&txn, &r_view.children[0].id, Some(uid.as_i64())) .unwrap(); assert_eq!(child_view.name, r_sub_view.name); assert_eq!(child_view.parent_view_id, r_sub_view.parent_view_id); - let views = folder.body.views.get_all_views(&txn, uid.as_i64()); + let views = folder.body.views.get_all_views(&txn, Some(uid.as_i64())); assert_eq!(views.len(), 3); } @@ -101,7 +101,7 @@ fn delete_view_test() { let views = folder .body .views - .get_views(&txn, &[v1_id, v2_id, v3_id], uid.as_i64()); + .get_views(&txn, &[v1_id, v2_id, v3_id], Some(uid.as_i64())); assert_eq!( views[0].id.to_string(), collab::entity::uuid_validation::view_id_from_any_string("v1").to_string() @@ -123,7 +123,7 @@ fn delete_view_test() { let views = folder .body .views - .get_views(&txn, &[v1_id, v2_id, v3_id], uid.as_i64()); + .get_views(&txn, &[v1_id, v2_id, v3_id], Some(uid.as_i64())); assert_eq!(views.len(), 0); } @@ -162,7 +162,7 @@ fn update_view_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.name, "Untitled"); assert!(r_view.is_favorite); @@ -203,7 +203,7 @@ fn update_view_icon_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.icon, Some(icon)); @@ -224,7 +224,7 @@ fn update_view_icon_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.icon, Some(new_icon)); folder @@ -240,7 +240,7 @@ fn update_view_icon_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.icon, None); assert_eq!(r_view.last_edited_by, Some(uid.as_i64())); @@ -278,7 +278,7 @@ fn different_icon_ty_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.icon, Some(emoji)); @@ -299,7 +299,7 @@ fn different_icon_ty_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.icon, Some(icon)); @@ -320,7 +320,7 @@ fn different_icon_ty_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.icon, Some(url)); } @@ -359,7 +359,7 @@ fn dissociate_and_associate_view_test() { let r_view = folder .body .views - .get_view(&txn, &v1_uuid, uid.as_i64()) + .get_view(&txn, &v1_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.children.items.iter().len(), 1); @@ -387,7 +387,7 @@ fn dissociate_and_associate_view_test() { let r_view = folder .body .views - .get_view(&txn, &v2_uuid, uid.as_i64()) + .get_view(&txn, &v2_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.children.items.iter().len(), 0); @@ -399,7 +399,7 @@ fn dissociate_and_associate_view_test() { let r_view = folder .body .views - .get_view(&txn, &v1_uuid, uid.as_i64()) + .get_view(&txn, &v1_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.children.items.iter().len(), 2); assert_eq!(r_view.children.items.first().unwrap().id, view_2_id); @@ -412,7 +412,7 @@ fn dissociate_and_associate_view_test() { let r_view = folder .body .views - .get_view(&txn, &v1_uuid, uid.as_i64()) + .get_view(&txn, &v1_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.children.items.iter().len(), 1); @@ -424,7 +424,7 @@ fn dissociate_and_associate_view_test() { let r_view = folder .body .views - .get_view(&txn, &v1_uuid, uid.as_i64()) + .get_view(&txn, &v1_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(r_view.children.items.iter().len(), 2); assert_eq!(r_view.children.items.first().unwrap().id, view_1_child_id); @@ -463,9 +463,9 @@ fn move_view_across_parent_test() { assert!(res.is_none()); // Move view_1_child from view_1 to view_2. folder.move_nested_view(&v1_child_uuid, &v2_uuid, None, uid.as_i64()); - let view_1 = folder.get_view(&v1_uuid, uid.as_i64()).unwrap(); - let view_2 = folder.get_view(&v2_uuid, uid.as_i64()).unwrap(); - let view_1_child = folder.get_view(&v1_child_uuid, uid.as_i64()).unwrap(); + let view_1 = folder.get_view(&v1_uuid, Some(uid.as_i64())).unwrap(); + let view_2 = folder.get_view(&v2_uuid, Some(uid.as_i64())).unwrap(); + let view_1_child = folder.get_view(&v1_child_uuid, Some(uid.as_i64())).unwrap(); assert_eq!(view_1.children.items.iter().len(), 0); assert_eq!(view_2.children.items.iter().len(), 1); assert_eq!(view_1_child.parent_view_id, Some(view_2_id)); @@ -479,11 +479,11 @@ fn move_view_across_parent_test() { None, uid.as_i64(), ); - let view_1 = folder.get_view(&v1_uuid, uid.as_i64()).unwrap(); - let view_2 = folder.get_view(&v2_uuid, uid.as_i64()).unwrap(); - let view_1_child = folder.get_view(&v1_child_uuid, uid.as_i64()).unwrap(); + let view_1 = folder.get_view(&v1_uuid, Some(uid.as_i64())).unwrap(); + let view_2 = folder.get_view(&v2_uuid, Some(uid.as_i64())).unwrap(); + let view_1_child = folder.get_view(&v1_child_uuid, Some(uid.as_i64())).unwrap(); let workspace = folder - .get_workspace_info(&workspace_uuid, uid.as_i64()) + .get_workspace_info(&workspace_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(view_1.children.items.iter().len(), 0); assert_eq!(view_2.children.items.iter().len(), 0); @@ -501,11 +501,11 @@ fn move_view_across_parent_test() { Some(view_1_id), uid.as_i64(), ); - let view_1 = folder.get_view(&v1_uuid, uid.as_i64()).unwrap(); - let view_2 = folder.get_view(&v2_uuid, uid.as_i64()).unwrap(); - let view_1_child = folder.get_view(&v1_child_uuid, uid.as_i64()).unwrap(); + let view_1 = folder.get_view(&v1_uuid, Some(uid.as_i64())).unwrap(); + let view_2 = folder.get_view(&v2_uuid, Some(uid.as_i64())).unwrap(); + let view_1_child = folder.get_view(&v1_child_uuid, Some(uid.as_i64())).unwrap(); let workspace = folder - .get_workspace_info(&workspace_uuid, uid.as_i64()) + .get_workspace_info(&workspace_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(view_1.children.items.iter().len(), 0); assert_eq!(view_2.children.items.iter().len(), 0); @@ -519,11 +519,11 @@ fn move_view_across_parent_test() { // move view_1_child from current workspace to view_1 folder.move_nested_view(&v1_child_uuid, &v1_uuid, None, uid.as_i64()); - let view_1 = folder.get_view(&v1_uuid, uid.as_i64()).unwrap(); - let view_2 = folder.get_view(&v2_uuid, uid.as_i64()).unwrap(); - let view_1_child = folder.get_view(&v1_child_uuid, uid.as_i64()).unwrap(); + let view_1 = folder.get_view(&v1_uuid, Some(uid.as_i64())).unwrap(); + let view_2 = folder.get_view(&v2_uuid, Some(uid.as_i64())).unwrap(); + let view_1_child = folder.get_view(&v1_child_uuid, Some(uid.as_i64())).unwrap(); let workspace = folder - .get_workspace_info(&workspace_uuid, uid.as_i64()) + .get_workspace_info(&workspace_uuid, Some(uid.as_i64())) .unwrap(); assert_eq!(view_1.children.items.iter().len(), 1); assert_eq!(view_1.children.items.first().unwrap().id, view_1_child_id); @@ -582,7 +582,7 @@ fn create_view_test_with_index() { let views = folder .body .views - .get_views_belong_to(&txn, &workspace_id, uid.as_i64()); + .get_views_belong_to(&txn, &workspace_id, Some(uid.as_i64())); assert_eq!(views.first().unwrap().id.to_string(), view_2.id.to_string()); assert_eq!(views.get(1).unwrap().id.to_string(), view_3.id.to_string()); assert_eq!(views.get(2).unwrap().id.to_string(), view_1.id.to_string()); @@ -608,7 +608,7 @@ fn check_created_and_edited_time_test() { let views = folder .body .views - .get_views_belong_to(&txn, &workspace_id, uid.as_i64()); + .get_views_belong_to(&txn, &workspace_id, Some(uid.as_i64())); let v1 = views.first().unwrap(); assert_eq!(v1.created_by.unwrap(), uid.as_i64()); assert_eq!(v1.last_edited_by.unwrap(), uid.as_i64()); @@ -621,7 +621,7 @@ async fn create_view_and_then_sub_index_content_test() { folder_test .folder .body - .observe_view_changes(uid.as_i64()) + .observe_view_changes(Some(uid.as_i64())) .await; let mut change_rx = folder_test @@ -656,7 +656,7 @@ async fn create_view_and_then_sub_index_content_test() { let r_view = folder .body .views - .get_view(&txn, &v1_id, uid.as_i64()) + .get_view(&txn, &v1_id, Some(uid.as_i64())) .unwrap(); assert_eq!(o_view.name, r_view.name); assert_eq!(o_view.parent_view_id, r_view.parent_view_id); diff --git a/collab/tests/importer/notion_test/import_test.rs b/collab/tests/importer/notion_test/import_test.rs index acb0cecdc..7371f3a0a 100644 --- a/collab/tests/importer/notion_test/import_test.rs +++ b/collab/tests/importer/notion_test/import_test.rs @@ -758,12 +758,12 @@ async fn import_level_test() { assert_eq!(view_hierarchy.flatten_views().len(), 14); folder.insert_nested_views(view_hierarchy.into_inner(), uid); - let first_level_views = folder.get_views_belong_to(&info.workspace_id, uid); + let first_level_views = folder.get_views_belong_to(&info.workspace_id, Some(uid)); assert_eq!(first_level_views.len(), 1); assert_eq!(first_level_views[0].children.len(), 3); println!("first_level_views: {:?}", first_level_views); - let second_level_views = folder.get_views_belong_to(&first_level_views[0].id, uid); + let second_level_views = folder.get_views_belong_to(&first_level_views[0].id, Some(uid)); verify_first_level_views(&second_level_views, &mut folder, uid); // Print out the views for debugging or manual inspection @@ -824,7 +824,7 @@ async fn import_empty_space() { // Helper function to verify second and third level views based on the first level view name fn verify_first_level_views(first_level_views: &[Arc], folder: &mut Folder, uid: i64) { for view in first_level_views { - let second_level_views = folder.get_views_belong_to(&view.id, uid); + let second_level_views = folder.get_views_belong_to(&view.id, Some(uid)); match view.name.as_str() { "Root2" => { assert_eq!(second_level_views.len(), 1); @@ -847,7 +847,7 @@ fn verify_first_level_views(first_level_views: &[Arc], folder: &mut Folder // Helper function to verify third level views based on the second level view name under "Root" fn verify_root_second_level_views(second_level_views: &[Arc], folder: &mut Folder, uid: i64) { for view in second_level_views { - let third_level_views = folder.get_views_belong_to(&view.id, uid); + let third_level_views = folder.get_views_belong_to(&view.id, Some(uid)); match view.name.as_str() { "root-2" => { assert_eq!(third_level_views.len(), 1); diff --git a/collab/tests/importer/workspace/folder_collab_remapper.rs b/collab/tests/importer/workspace/folder_collab_remapper.rs index b8eb2df28..dc044b4b3 100644 --- a/collab/tests/importer/workspace/folder_collab_remapper.rs +++ b/collab/tests/importer/workspace/folder_collab_remapper.rs @@ -16,7 +16,7 @@ fn verify_view( uid: i64, ) { let new_id = id_mapper.get_new_id(old_id).unwrap(); - let view = folder.get_view(&new_id, uid).unwrap(); + let view = folder.get_view(&new_id, Some(uid)).unwrap(); assert_eq!(view.name, expected_name); assert_eq!( @@ -60,7 +60,7 @@ async fn test_folder_collab_remapper() { .unwrap() ); - let workspace_info = folder.get_workspace_info(&workspace_id, uid).unwrap(); + let workspace_info = folder.get_workspace_info(&workspace_id, Some(uid)).unwrap(); assert_eq!(workspace_info.name, "My Custom Workspace"); assert_eq!(workspace_info.id, workspace_id); @@ -76,7 +76,7 @@ async fn test_folder_collab_remapper() { .count(); assert_eq!(workspace_info.child_views.len(), top_level_views_count); - let all_views = folder.get_all_views(uid); + let all_views = folder.get_all_views(Some(uid)); // +1: workspace is also a view assert_eq!(all_views.len(), relation_map.views.len() + 1); @@ -223,6 +223,6 @@ async fn test_folder_hierarchy_structure() { uid, ); - let child_views = folder.get_views_belong_to(&getting_started_new_id, uid); + let child_views = folder.get_views_belong_to(&getting_started_new_id, Some(uid)); assert_eq!(child_views.len(), 3); } diff --git a/collab/tests/importer/workspace/space_view_edge_case_handler.rs b/collab/tests/importer/workspace/space_view_edge_case_handler.rs index 6769d5ba2..6f1876207 100644 --- a/collab/tests/importer/workspace/space_view_edge_case_handler.rs +++ b/collab/tests/importer/workspace/space_view_edge_case_handler.rs @@ -88,7 +88,7 @@ async fn test_space_view_edge_case_handler() { "folder should use custom workspace id" ); - let all_views = folder.get_all_views(uid); + let all_views = folder.get_all_views(Some(uid)); let space_view_found = all_views.iter().any(|view| { if let Some(extra) = &view.extra { if let Ok(space_info) = serde_json::from_str::(extra) { diff --git a/collab/tests/importer/workspace/workspace_remapper_test.rs b/collab/tests/importer/workspace/workspace_remapper_test.rs index fa0e29907..97e3318c2 100644 --- a/collab/tests/importer/workspace/workspace_remapper_test.rs +++ b/collab/tests/importer/workspace/workspace_remapper_test.rs @@ -41,12 +41,12 @@ async fn test_workspace_remapper_folder_structure() { let folder = remapper.build_folder_collab(uid, workspace_name).unwrap(); let workspace_id = folder.get_workspace_id().unwrap(); - let workspace_info = folder.get_workspace_info(&workspace_id, uid).unwrap(); + let workspace_info = folder.get_workspace_info(&workspace_id, Some(uid)).unwrap(); assert_eq!(workspace_info.name, workspace_name); assert_eq!(workspace_info.id, workspace_id); - let all_views = folder.get_all_views(uid); + let all_views = folder.get_all_views(Some(uid)); assert_eq!(all_views.len(), 8); }