gitdataai/libs/service/project/board.rs
2026-04-14 19:02:01 +08:00

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(())
}
}