diff options
Diffstat (limited to 'src/app/api/workspaces.rs')
| -rw-r--r-- | src/app/api/workspaces.rs | 126 |
1 files changed, 125 insertions, 1 deletions
diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs index b7260c17..cf50d3ad 100644 --- a/src/app/api/workspaces.rs +++ b/src/app/api/workspaces.rs @@ -2,7 +2,8 @@ use std::path::PathBuf; use crate::api::schema::{ EventData, EventEnvelope, EventKind, ResponseResult, WorkspaceCreateParams, - WorkspaceMoveParams, WorkspaceRenameParams, WorkspaceReportMetadataParams, WorkspaceTarget, + WorkspaceMoveBlockParams, WorkspaceMoveParams, WorkspaceRenameParams, + WorkspaceReportMetadataParams, WorkspaceTarget, }; use crate::app::App; @@ -154,6 +155,76 @@ impl App { encode_success(id, ResponseResult::WorkspaceList { workspaces }) } + pub(super) fn handle_workspace_move_block( + &mut self, + id: String, + params: WorkspaceMoveBlockParams, + ) -> String { + if params.workspace_ids.is_empty() { + return encode_error( + id, + "workspace_move_block_failed", + "workspace_ids must not be empty", + ); + } + + let mut workspace_ids = Vec::with_capacity(params.workspace_ids.len()); + let mut seen_ids = std::collections::HashSet::new(); + for requested_id in ¶ms.workspace_ids { + let Some(index) = self.parse_workspace_id(requested_id) else { + return workspace_not_found(id, requested_id); + }; + let Some(workspace) = self.state.workspaces.get(index) else { + return workspace_not_found(id, requested_id); + }; + if !seen_ids.insert(workspace.id.clone()) { + return encode_error( + id, + "workspace_move_block_failed", + format!("workspace {requested_id} appears more than once"), + ); + } + workspace_ids.push(workspace.id.clone()); + } + + let before_workspace_id = match params.before_workspace_id { + Some(requested_id) => { + let Some(index) = self.parse_workspace_id(&requested_id) else { + return workspace_not_found(id, &requested_id); + }; + let Some(workspace) = self.state.workspaces.get(index) else { + return workspace_not_found(id, &requested_id); + }; + if seen_ids.contains(&workspace.id) { + return encode_error( + id, + "workspace_move_block_failed", + "before_workspace_id must not be part of workspace_ids", + ); + } + Some(workspace.id.clone()) + } + None => None, + }; + + let moved = self + .state + .move_workspace_block(&workspace_ids, before_workspace_id.as_deref()); + let workspaces = self.workspace_list_info(); + if moved { + self.emit_event(EventEnvelope { + event: EventKind::WorkspaceReordered, + data: EventData::WorkspaceReordered { + workspace_ids, + before_workspace_id, + workspaces: workspaces.clone(), + }, + }); + } + + encode_success(id, ResponseResult::WorkspaceList { workspaces }) + } + pub(super) fn handle_workspace_report_metadata( &mut self, id: String, @@ -561,6 +632,59 @@ mod tests { } #[test] + fn api_workspace_move_block_reorders_atomically() { + let event_hub = crate::api::EventHub::default(); + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone()); + app.state.workspaces = vec![ + Workspace::test_new("child"), + Workspace::test_new("normal"), + Workspace::test_new("parent"), + Workspace::test_new("tail"), + ]; + let parent_id = app.public_workspace_id(2); + let child_id = app.public_workspace_id(0); + let tail_id = app.public_workspace_id(3); + + let response = app.handle_workspace_move_block( + "req".into(), + WorkspaceMoveBlockParams { + workspace_ids: vec![parent_id.clone(), child_id.clone()], + before_workspace_id: Some(tail_id.clone()), + }, + ); + + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + let ResponseResult::WorkspaceList { workspaces } = success.result else { + panic!("expected workspace list"); + }; + assert_eq!( + app.state + .workspaces + .iter() + .map(|workspace| workspace.display_name()) + .collect::<Vec<_>>(), + ["normal", "parent", "child", "tail"] + ); + assert_eq!(workspaces[1].workspace_id, parent_id); + assert_eq!(workspaces[2].workspace_id, child_id); + let events = event_hub.events_after(0); + assert_eq!(events.len(), 1); + assert!(matches!( + &events[0].1.data, + EventData::WorkspaceReordered { + workspace_ids, + before_workspace_id, + workspaces, + } if workspace_ids.first() == Some(&parent_id) + && workspace_ids.get(1) == Some(&child_id) + && workspace_ids.len() == 2 + && before_workspace_id.as_deref() == Some(tail_id.as_str()) + && workspaces[1].workspace_id == parent_id + )); + } + + #[test] fn api_workspace_move_noop_does_not_emit_event() { let event_hub = crate::api::EventHub::default(); let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); |
