497 lines
16 KiB
Rust
497 lines
16 KiB
Rust
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use chrono::Utc;
|
|
use models::projects::{project_board, project_board_card, project_board_column};
|
|
use sea_orm::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct CreateBoardParams {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct UpdateBoardParams {
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BoardResponse {
|
|
pub id: Uuid,
|
|
pub project: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub created_by: Uuid,
|
|
pub created_at: chrono::DateTime<Utc>,
|
|
pub updated_at: chrono::DateTime<Utc>,
|
|
}
|
|
|
|
impl From<project_board::Model> for BoardResponse {
|
|
fn from(m: project_board::Model) -> Self {
|
|
Self {
|
|
id: m.id,
|
|
project: m.project,
|
|
name: m.name,
|
|
description: m.description,
|
|
created_by: m.created_by,
|
|
created_at: m.created_at,
|
|
updated_at: m.updated_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct CreateColumnParams {
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub position: i32,
|
|
pub wip_limit: Option<i32>,
|
|
pub color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct UpdateColumnParams {
|
|
pub name: Option<String>,
|
|
pub position: Option<i32>,
|
|
pub wip_limit: Option<i32>,
|
|
pub color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ColumnResponse {
|
|
pub id: Uuid,
|
|
pub board: Uuid,
|
|
pub name: String,
|
|
pub position: i32,
|
|
pub wip_limit: Option<i32>,
|
|
pub color: Option<String>,
|
|
}
|
|
|
|
impl From<project_board_column::Model> for ColumnResponse {
|
|
fn from(m: project_board_column::Model) -> Self {
|
|
Self {
|
|
id: m.id,
|
|
board: m.board,
|
|
name: m.name,
|
|
position: m.position,
|
|
wip_limit: m.wip_limit,
|
|
color: m.color,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct CreateCardParams {
|
|
pub column_id: Uuid,
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
pub issue_id: Option<i64>,
|
|
pub assignee_id: Option<Uuid>,
|
|
pub due_date: Option<chrono::DateTime<Utc>>,
|
|
pub priority: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct UpdateCardParams {
|
|
pub title: Option<String>,
|
|
pub description: Option<String>,
|
|
pub assignee_id: Option<Uuid>,
|
|
pub due_date: Option<chrono::DateTime<Utc>>,
|
|
pub priority: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct MoveCardParams {
|
|
pub target_column_id: Uuid,
|
|
pub position: i32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct CardResponse {
|
|
pub id: Uuid,
|
|
pub column: Uuid,
|
|
pub issue_id: Option<i64>,
|
|
pub project: Option<Uuid>,
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
pub position: i32,
|
|
pub assignee_id: Option<Uuid>,
|
|
pub due_date: Option<chrono::DateTime<Utc>>,
|
|
pub priority: Option<String>,
|
|
pub created_by: Uuid,
|
|
pub created_at: chrono::DateTime<Utc>,
|
|
pub updated_at: chrono::DateTime<Utc>,
|
|
}
|
|
|
|
impl From<project_board_card::Model> for CardResponse {
|
|
fn from(m: project_board_card::Model) -> Self {
|
|
Self {
|
|
id: m.id,
|
|
column: m.column,
|
|
issue_id: m.issue_id,
|
|
project: m.project,
|
|
title: m.title,
|
|
description: m.description,
|
|
position: m.position,
|
|
assignee_id: m.assignee_id,
|
|
due_date: m.due_date,
|
|
priority: m.priority,
|
|
created_by: m.created_by,
|
|
created_at: m.created_at,
|
|
updated_at: m.updated_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BoardWithColumnsResponse {
|
|
pub board: BoardResponse,
|
|
pub columns: Vec<ColumnWithCardsResponse>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ColumnWithCardsResponse {
|
|
pub column: ColumnResponse,
|
|
pub cards: Vec<CardResponse>,
|
|
}
|
|
|
|
impl AppService {
|
|
/// List all boards for a project.
|
|
pub async fn board_list(
|
|
&self,
|
|
project_name: String,
|
|
ctx: &Session,
|
|
) -> Result<Vec<BoardResponse>, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self.utils_find_project_by_name(project_name).await?;
|
|
let boards = project_board::Entity::find()
|
|
.filter(project_board::Column::Project.eq(project.id))
|
|
.order_by_asc(project_board::Column::CreatedAt)
|
|
.all(&self.db)
|
|
.await?;
|
|
Ok(boards.into_iter().map(BoardResponse::from).collect())
|
|
}
|
|
|
|
pub async fn board_get(
|
|
&self,
|
|
project_name: String,
|
|
board_id: Uuid,
|
|
ctx: &Session,
|
|
) -> Result<BoardWithColumnsResponse, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let board = project_board::Entity::find_by_id(board_id)
|
|
.filter(project_board::Column::Project.eq(project.id))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Board not found".to_string()))?;
|
|
|
|
let columns = project_board_column::Entity::find()
|
|
.filter(project_board_column::Column::Board.eq(board_id))
|
|
.order_by_asc(project_board_column::Column::Position)
|
|
.all(&self.db)
|
|
.await?;
|
|
|
|
let column_ids: Vec<Uuid> = columns.iter().map(|c| c.id).collect();
|
|
let cards = if column_ids.is_empty() {
|
|
vec![]
|
|
} else {
|
|
project_board_card::Entity::find()
|
|
.filter(project_board_card::Column::Column.is_in(column_ids))
|
|
.order_by_asc(project_board_card::Column::Position)
|
|
.all(&self.db)
|
|
.await?
|
|
};
|
|
|
|
let columns_with_cards: Vec<ColumnWithCardsResponse> = columns
|
|
.into_iter()
|
|
.map(|c| {
|
|
let col_cards: Vec<CardResponse> = cards
|
|
.iter()
|
|
.filter(|card| card.column == c.id)
|
|
.cloned()
|
|
.map(CardResponse::from)
|
|
.collect();
|
|
ColumnWithCardsResponse {
|
|
column: ColumnResponse::from(c),
|
|
cards: col_cards,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(BoardWithColumnsResponse {
|
|
board: BoardResponse::from(board),
|
|
columns: columns_with_cards,
|
|
})
|
|
}
|
|
|
|
/// Create a new board.
|
|
pub async fn board_create(
|
|
&self,
|
|
project_name: String,
|
|
params: CreateBoardParams,
|
|
ctx: &Session,
|
|
) -> Result<BoardResponse, AppError> {
|
|
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self.utils_find_project_by_name(project_name).await?;
|
|
let now = Utc::now();
|
|
|
|
let active = project_board::ActiveModel {
|
|
id: Set(Uuid::new_v4()),
|
|
project: Set(project.id),
|
|
name: Set(params.name),
|
|
description: Set(params.description),
|
|
created_by: Set(user_uid),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
let board = active.insert(&self.db).await?;
|
|
Ok(BoardResponse::from(board))
|
|
}
|
|
|
|
/// Update a board.
|
|
pub async fn board_update(
|
|
&self,
|
|
project_name: String,
|
|
board_id: Uuid,
|
|
params: UpdateBoardParams,
|
|
ctx: &Session,
|
|
) -> Result<BoardResponse, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let board = project_board::Entity::find_by_id(board_id)
|
|
.filter(project_board::Column::Project.eq(project.id))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Board not found".to_string()))?;
|
|
|
|
let mut active: project_board::ActiveModel = board.into();
|
|
if let Some(v) = params.name {
|
|
active.name = Set(v);
|
|
}
|
|
if params.description.is_some() {
|
|
active.description = Set(params.description);
|
|
}
|
|
active.updated_at = Set(Utc::now());
|
|
let updated = active.update(&self.db).await?;
|
|
Ok(BoardResponse::from(updated))
|
|
}
|
|
|
|
/// Delete a board.
|
|
pub async fn board_delete(
|
|
&self,
|
|
project_name: String,
|
|
board_id: Uuid,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let deleted = project_board::Entity::delete_by_id(board_id)
|
|
.filter(project_board::Column::Project.eq(project.id))
|
|
.exec(&self.db)
|
|
.await?;
|
|
if deleted.rows_affected == 0 {
|
|
return Err(AppError::NotFound("Board not found".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a column on a board.
|
|
pub async fn column_create(
|
|
&self,
|
|
project_name: String,
|
|
board_id: Uuid,
|
|
params: CreateColumnParams,
|
|
ctx: &Session,
|
|
) -> Result<ColumnResponse, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let board = project_board::Entity::find_by_id(board_id)
|
|
.filter(project_board::Column::Project.eq(project.id))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Board not found".to_string()))?;
|
|
|
|
let active = project_board_column::ActiveModel {
|
|
id: Set(Uuid::new_v4()),
|
|
board: Set(board.id),
|
|
name: Set(params.name),
|
|
position: Set(params.position),
|
|
wip_limit: Set(params.wip_limit),
|
|
color: Set(params.color),
|
|
};
|
|
let column = active.insert(&self.db).await?;
|
|
Ok(ColumnResponse::from(column))
|
|
}
|
|
|
|
/// Update a column.
|
|
pub async fn column_update(
|
|
&self,
|
|
project_name: String,
|
|
column_id: Uuid,
|
|
params: UpdateColumnParams,
|
|
ctx: &Session,
|
|
) -> Result<ColumnResponse, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let _project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let column = project_board_column::Entity::find_by_id(column_id)
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Column not found".to_string()))?;
|
|
|
|
let mut active: project_board_column::ActiveModel = column.into();
|
|
if let Some(v) = params.name {
|
|
active.name = Set(v);
|
|
}
|
|
if let Some(v) = params.position {
|
|
active.position = Set(v);
|
|
}
|
|
if params.wip_limit.is_some() {
|
|
active.wip_limit = Set(params.wip_limit);
|
|
}
|
|
if params.color.is_some() {
|
|
active.color = Set(params.color);
|
|
}
|
|
let updated = active.update(&self.db).await?;
|
|
Ok(ColumnResponse::from(updated))
|
|
}
|
|
|
|
/// Delete a column.
|
|
pub async fn column_delete(
|
|
&self,
|
|
project_name: String,
|
|
column_id: Uuid,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let _project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let deleted = project_board_column::Entity::delete_by_id(column_id)
|
|
.exec(&self.db)
|
|
.await?;
|
|
if deleted.rows_affected == 0 {
|
|
return Err(AppError::NotFound("Column not found".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a card in a column.
|
|
pub async fn card_create(
|
|
&self,
|
|
project_name: String,
|
|
params: CreateCardParams,
|
|
ctx: &Session,
|
|
) -> Result<CardResponse, AppError> {
|
|
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self.utils_find_project_by_name(project_name).await?;
|
|
let now = Utc::now();
|
|
|
|
let active = project_board_card::ActiveModel {
|
|
id: Set(Uuid::new_v4()),
|
|
column: Set(params.column_id),
|
|
issue_id: Set(params.issue_id),
|
|
project: Set(Some(project.id)),
|
|
title: Set(params.title),
|
|
description: Set(params.description),
|
|
position: Set(0),
|
|
assignee_id: Set(params.assignee_id),
|
|
due_date: Set(params.due_date),
|
|
priority: Set(params.priority),
|
|
created_by: Set(user_uid),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
let card = active.insert(&self.db).await?;
|
|
Ok(CardResponse::from(card))
|
|
}
|
|
|
|
/// Update a card.
|
|
pub async fn card_update(
|
|
&self,
|
|
project_name: String,
|
|
card_id: Uuid,
|
|
params: UpdateCardParams,
|
|
ctx: &Session,
|
|
) -> Result<CardResponse, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let _project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let card = project_board_card::Entity::find_by_id(card_id)
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Card not found".to_string()))?;
|
|
|
|
let mut active: project_board_card::ActiveModel = card.into();
|
|
if let Some(v) = params.title {
|
|
active.title = Set(v);
|
|
}
|
|
if params.description.is_some() {
|
|
active.description = Set(params.description);
|
|
}
|
|
if params.assignee_id.is_some() {
|
|
active.assignee_id = Set(params.assignee_id);
|
|
}
|
|
if params.due_date.is_some() {
|
|
active.due_date = Set(params.due_date);
|
|
}
|
|
if params.priority.is_some() {
|
|
active.priority = Set(params.priority);
|
|
}
|
|
active.updated_at = Set(Utc::now());
|
|
let updated = active.update(&self.db).await?;
|
|
Ok(CardResponse::from(updated))
|
|
}
|
|
|
|
/// Move a card to a different column and/or position.
|
|
pub async fn card_move(
|
|
&self,
|
|
project_name: String,
|
|
card_id: Uuid,
|
|
params: MoveCardParams,
|
|
ctx: &Session,
|
|
) -> Result<CardResponse, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let _project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let card = project_board_card::Entity::find_by_id(card_id)
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Card not found".to_string()))?;
|
|
|
|
let mut active: project_board_card::ActiveModel = card.into();
|
|
active.column = Set(params.target_column_id);
|
|
active.position = Set(params.position);
|
|
active.updated_at = Set(Utc::now());
|
|
let updated = active.update(&self.db).await?;
|
|
Ok(CardResponse::from(updated))
|
|
}
|
|
|
|
/// Delete a card.
|
|
pub async fn card_delete(
|
|
&self,
|
|
project_name: String,
|
|
card_id: Uuid,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let _project = self.utils_find_project_by_name(project_name).await?;
|
|
|
|
let deleted = project_board_card::Entity::delete_by_id(card_id)
|
|
.exec(&self.db)
|
|
.await?;
|
|
if deleted.rows_affected == 0 {
|
|
return Err(AppError::NotFound("Card not found".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|