//! Named-layout management use cases (#4): list, create, rename, delete and set //! the active layout. Each loads the project's layouts store (see //! [`super::store`]), applies the change and persists it. use std::sync::Arc; use domain::ports::{EventBus, FileSystem, IdGenerator, ProjectStore}; use domain::{DomainEvent, LayoutId, ProjectId}; use crate::error::AppError; use super::store::{default_tree, persist_doc, resolve_doc, LayoutKind, NamedLayout}; /// Lightweight descriptor of a named layout (no tree), for the layouts tab bar. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LayoutInfo { /// Stable identifier. pub id: LayoutId, /// Display name. pub name: String, /// Kind of this layout. pub kind: LayoutKind, } // --------------------------------------------------------------------------- // ListLayouts // --------------------------------------------------------------------------- /// Input for [`ListLayouts::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListLayoutsInput { /// Project whose layouts to list. pub project_id: ProjectId, } /// Output of [`ListLayouts::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListLayoutsOutput { /// All named layouts (id + name), in order. pub layouts: Vec, /// The active layout. pub active_id: LayoutId, } /// Lists a project's named layouts and the active one. pub struct ListLayouts { store: Arc, fs: Arc, } impl ListLayouts { /// Builds the use case from its ports. #[must_use] pub fn new(store: Arc, fs: Arc) -> Self { Self { store, fs } } /// Lists the layouts. /// /// # Errors /// [`AppError::NotFound`] for an unknown project, [`AppError::FileSystem`] / /// [`AppError::Store`] on I/O failure. pub async fn execute(&self, input: ListLayoutsInput) -> Result { let project = self.store.load_project(input.project_id).await?; let doc = resolve_doc(self.fs.as_ref(), &project).await?; Ok(ListLayoutsOutput { layouts: doc .layouts .iter() .map(|l| LayoutInfo { id: l.id, name: l.name.clone(), kind: l.kind, }) .collect(), active_id: doc.active_id, }) } } // --------------------------------------------------------------------------- // CreateLayout // --------------------------------------------------------------------------- /// Input for [`CreateLayout::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateLayoutInput { /// Owning project. pub project_id: ProjectId, /// Display name for the new layout. pub name: String, /// Kind of the new layout (defaults to Terminal). pub kind: LayoutKind, } /// Output of [`CreateLayout::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateLayoutOutput { /// The id minted for the new (now active) layout. pub layout_id: LayoutId, } /// Creates a new empty named layout and makes it active. pub struct CreateLayout { store: Arc, fs: Arc, ids: Arc, events: Arc, } impl CreateLayout { /// Builds the use case from its ports. #[must_use] pub fn new( store: Arc, fs: Arc, ids: Arc, events: Arc, ) -> Self { Self { store, fs, ids, events, } } /// Creates the layout. /// /// # Errors /// [`AppError::Invalid`] for an empty name, [`AppError::NotFound`] for an /// unknown project, I/O errors otherwise. pub async fn execute(&self, input: CreateLayoutInput) -> Result { let name = input.name.trim(); if name.is_empty() { return Err(AppError::Invalid("layout name is empty".to_owned())); } let project = self.store.load_project(input.project_id).await?; let mut doc = resolve_doc(self.fs.as_ref(), &project).await?; let id = LayoutId::from_uuid(self.ids.new_uuid()); doc.layouts.push(NamedLayout { id, name: name.to_owned(), kind: input.kind, tree: default_tree(), }); doc.active_id = id; // a freshly-created layout becomes active. persist_doc(self.fs.as_ref(), &project, &doc).await?; self.events.publish(DomainEvent::LayoutChanged { project_id: input.project_id, }); Ok(CreateLayoutOutput { layout_id: id }) } } // --------------------------------------------------------------------------- // RenameLayout // --------------------------------------------------------------------------- /// Input for [`RenameLayout::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RenameLayoutInput { /// Owning project. pub project_id: ProjectId, /// Layout to rename. pub layout_id: LayoutId, /// New display name. pub name: String, } /// Renames a named layout. pub struct RenameLayout { store: Arc, fs: Arc, events: Arc, } impl RenameLayout { /// Builds the use case from its ports. #[must_use] pub fn new( store: Arc, fs: Arc, events: Arc, ) -> Self { Self { store, fs, events } } /// Renames the layout. /// /// # Errors /// [`AppError::Invalid`] for an empty name, [`AppError::NotFound`] if the /// project or layout is unknown. pub async fn execute(&self, input: RenameLayoutInput) -> Result<(), AppError> { let name = input.name.trim(); if name.is_empty() { return Err(AppError::Invalid("layout name is empty".to_owned())); } let project = self.store.load_project(input.project_id).await?; let mut doc = resolve_doc(self.fs.as_ref(), &project).await?; let named = doc .find_mut(input.layout_id) .ok_or_else(|| AppError::NotFound(format!("layout {}", input.layout_id)))?; named.name = name.to_owned(); persist_doc(self.fs.as_ref(), &project, &doc).await?; self.events.publish(DomainEvent::LayoutChanged { project_id: input.project_id, }); Ok(()) } } // --------------------------------------------------------------------------- // DeleteLayout // --------------------------------------------------------------------------- /// Input for [`DeleteLayout::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteLayoutInput { /// Owning project. pub project_id: ProjectId, /// Layout to delete. pub layout_id: LayoutId, } /// Output of [`DeleteLayout::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteLayoutOutput { /// The active layout after the deletion. pub active_id: LayoutId, } /// Deletes a named layout. The last remaining layout cannot be deleted; if the /// active layout is removed, the first remaining one becomes active. pub struct DeleteLayout { store: Arc, fs: Arc, events: Arc, } impl DeleteLayout { /// Builds the use case from its ports. #[must_use] pub fn new( store: Arc, fs: Arc, events: Arc, ) -> Self { Self { store, fs, events } } /// Deletes the layout. /// /// # Errors /// [`AppError::Invalid`] if it is the last layout, [`AppError::NotFound`] if /// the project or layout is unknown. pub async fn execute(&self, input: DeleteLayoutInput) -> Result { let project = self.store.load_project(input.project_id).await?; let mut doc = resolve_doc(self.fs.as_ref(), &project).await?; if doc.layouts.len() <= 1 { return Err(AppError::Invalid( "cannot delete the last layout".to_owned(), )); } if doc.find(input.layout_id).is_none() { return Err(AppError::NotFound(format!("layout {}", input.layout_id))); } doc.layouts.retain(|l| l.id != input.layout_id); if doc.active_id == input.layout_id { doc.active_id = doc.layouts[0].id; } persist_doc(self.fs.as_ref(), &project, &doc).await?; self.events.publish(DomainEvent::LayoutChanged { project_id: input.project_id, }); Ok(DeleteLayoutOutput { active_id: doc.active_id, }) } } // --------------------------------------------------------------------------- // SetActiveLayout // --------------------------------------------------------------------------- /// Input for [`SetActiveLayout::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetActiveLayoutInput { /// Owning project. pub project_id: ProjectId, /// Layout to make active. pub layout_id: LayoutId, } /// Switches the active layout of a project. pub struct SetActiveLayout { store: Arc, fs: Arc, events: Arc, } impl SetActiveLayout { /// Builds the use case from its ports. #[must_use] pub fn new( store: Arc, fs: Arc, events: Arc, ) -> Self { Self { store, fs, events } } /// Sets the active layout. /// /// # Errors /// [`AppError::NotFound`] if the project or layout is unknown. pub async fn execute(&self, input: SetActiveLayoutInput) -> Result<(), AppError> { let project = self.store.load_project(input.project_id).await?; let mut doc = resolve_doc(self.fs.as_ref(), &project).await?; if doc.find(input.layout_id).is_none() { return Err(AppError::NotFound(format!("layout {}", input.layout_id))); } doc.active_id = input.layout_id; persist_doc(self.fs.as_ref(), &project, &doc).await?; self.events.publish(DomainEvent::LayoutChanged { project_id: input.project_id, }); Ok(()) } }