feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
336
crates/application/src/layout/management.rs
Normal file
336
crates/application/src/layout/management.rs
Normal file
@ -0,0 +1,336 @@
|
||||
//! 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<LayoutInfo>,
|
||||
/// The active layout.
|
||||
pub active_id: LayoutId,
|
||||
}
|
||||
|
||||
/// Lists a project's named layouts and the active one.
|
||||
pub struct ListLayouts {
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl ListLayouts {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn ProjectStore>, fs: Arc<dyn FileSystem>) -> 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<ListLayoutsOutput, AppError> {
|
||||
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<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl CreateLayout {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> 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<CreateLayoutOutput, 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 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<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl RenameLayout {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> 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<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl DeleteLayout {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> 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<DeleteLayoutOutput, AppError> {
|
||||
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<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl SetActiveLayout {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user