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) <noreply@anthropic.com>
This commit is contained in:
Tristan Michael 2026-03-31 13:27:47 -07:00 committed by GitButler
parent 6959b1f44f
commit c4c03679ae
2 changed files with 82 additions and 0 deletions

View file

@ -68,6 +68,17 @@ impl TaskRepository {
self.storage.delete_list(list_id) 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 // Task ordering
pub fn reorder_task(&mut self, list_id: Uuid, task_id: Uuid, new_position: usize) -> Result<()> { 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)?; let mut metadata = self.storage.read_list_metadata(list_id)?;
@ -320,6 +331,54 @@ mod tests {
assert!(lists.is_empty()); 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] #[test]
fn test_delete_list_removes_from_root_metadata() { fn test_delete_list_removes_from_root_metadata() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();

View file

@ -88,6 +88,8 @@ pub trait Storage {
fn read_root_metadata(&self) -> Result<RootMetadata>; fn read_root_metadata(&self) -> Result<RootMetadata>;
fn write_root_metadata(&mut self, metadata: &RootMetadata) -> 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<ListMetadata>; fn read_list_metadata(&self, list_id: Uuid) -> Result<ListMetadata>;
fn write_list_metadata(&mut self, metadata: &ListMetadata) -> Result<()>; fn write_list_metadata(&mut self, metadata: &ListMetadata) -> Result<()>;
} }
@ -464,6 +466,27 @@ impl Storage for FileSystemStorage {
Ok(()) 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<RootMetadata> { fn read_root_metadata(&self) -> Result<RootMetadata> {
self.read_root_metadata_internal() self.read_root_metadata_internal()
} }