From c4c03679ae2bc215582b696539873e4e2b35d3a7 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 13:27:47 -0700 Subject: [PATCH] feat(core): add move_task and rename_list to onyx-core Add TaskRepository::move_task() to move tasks between lists and rename_list() to rename lists on the filesystem. Adds rename_list to the Storage trait with FileSystemStorage implementation. Includes tests for both operations plus duplicate-name error case. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/onyx-core/src/repository.rs | 59 ++++++++++++++++++++++++++++++ crates/onyx-core/src/storage.rs | 23 ++++++++++++ 2 files changed, 82 insertions(+) diff --git a/crates/onyx-core/src/repository.rs b/crates/onyx-core/src/repository.rs index d8492f3..3ba3ea2 100644 --- a/crates/onyx-core/src/repository.rs +++ b/crates/onyx-core/src/repository.rs @@ -68,6 +68,17 @@ impl TaskRepository { self.storage.delete_list(list_id) } + pub fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> { + self.storage.rename_list(list_id, new_name) + } + + pub fn move_task(&mut self, from_list_id: Uuid, to_list_id: Uuid, task_id: Uuid) -> Result<()> { + let task = self.storage.read_task(from_list_id, task_id)?; + self.storage.write_task(to_list_id, &task)?; + self.storage.delete_task(from_list_id, task_id)?; + Ok(()) + } + // Task ordering pub fn reorder_task(&mut self, list_id: Uuid, task_id: Uuid, new_position: usize) -> Result<()> { let mut metadata = self.storage.read_list_metadata(list_id)?; @@ -320,6 +331,54 @@ mod tests { assert!(lists.is_empty()); } + #[test] + fn test_move_task_between_lists() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + + let list_a = repo.create_list("List A".to_string()).unwrap(); + let list_b = repo.create_list("List B".to_string()).unwrap(); + let task = repo.create_task(list_a.id, Task::new("Movable".to_string())).unwrap(); + + repo.move_task(list_a.id, list_b.id, task.id).unwrap(); + + let tasks_a = repo.list_tasks(list_a.id).unwrap(); + assert_eq!(tasks_a.len(), 0); + + let tasks_b = repo.list_tasks(list_b.id).unwrap(); + assert_eq!(tasks_b.len(), 1); + assert_eq!(tasks_b[0].title, "Movable"); + } + + #[test] + fn test_rename_list() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + + let list = repo.create_list("Old Name".to_string()).unwrap(); + repo.rename_list(list.id, "New Name".to_string()).unwrap(); + + let renamed = repo.get_list(list.id).unwrap(); + assert_eq!(renamed.title, "New Name"); + + // Old directory should be gone + assert!(!temp_dir.path().join("Old Name").exists()); + assert!(temp_dir.path().join("New Name").exists()); + } + + #[test] + fn test_rename_list_duplicate_name() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + + repo.create_list("A".to_string()).unwrap(); + let list_b = repo.create_list("B".to_string()).unwrap(); + + let result = repo.rename_list(list_b.id, "A".to_string()); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::InvalidData(_))); + } + #[test] fn test_delete_list_removes_from_root_metadata() { let temp_dir = TempDir::new().unwrap(); diff --git a/crates/onyx-core/src/storage.rs b/crates/onyx-core/src/storage.rs index e6c6a4b..2fda73a 100644 --- a/crates/onyx-core/src/storage.rs +++ b/crates/onyx-core/src/storage.rs @@ -88,6 +88,8 @@ pub trait Storage { fn read_root_metadata(&self) -> Result; fn write_root_metadata(&mut self, metadata: &RootMetadata) -> Result<()>; + fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()>; + fn read_list_metadata(&self, list_id: Uuid) -> Result; fn write_list_metadata(&mut self, metadata: &ListMetadata) -> Result<()>; } @@ -464,6 +466,27 @@ impl Storage for FileSystemStorage { Ok(()) } + fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> { + let old_dir = self.list_dir_path(list_id)?; + let new_dir = self.list_dir_path_by_name(&new_name); + + if new_dir.exists() { + return Err(Error::InvalidData(format!("A list named '{}' already exists", new_name))); + } + + fs::rename(&old_dir, &new_dir)?; + + // Update metadata timestamp + let metadata_path = new_dir.join(".listdata.json"); + let content = fs::read_to_string(&metadata_path)?; + let mut metadata: ListMetadata = serde_json::from_str(&content)?; + metadata.updated_at = Utc::now(); + let json = serde_json::to_string_pretty(&metadata)?; + fs::write(&metadata_path, json)?; + + Ok(()) + } + fn read_root_metadata(&self) -> Result { self.read_root_metadata_internal() }