aboutsummaryrefslogtreecommitdiff
path: root/src/app/api/workspaces.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/api/workspaces.rs')
-rw-r--r--src/app/api/workspaces.rs126
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 &params.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();