use models::issue::issue_pin; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; use crate::errors::IssueError; use crate::services::service::IssueService; use crate::share::authz; use crate::share::lookup::{board_context, lookup_board, lookup_issue, space_context}; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct PinDto { pub id: Uuid, pub board_id: Uuid, pub issue_id: Option, pub pin_order: i32, pub created_at: OffsetDateTime, } impl From for PinDto { fn from(m: issue_pin::Model) -> Self { Self { id: m.id, board_id: m.collection_id, issue_id: m.issue_id, pin_order: m.pin_order, created_at: m.created_at, } } } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct CreatePinCmd { pub issue_id: Uuid, pub pin_order: i32, } #[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)] pub struct PinPath { pub space_name: String, pub board_id: Uuid, pub pin_id: Uuid, } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct UpdatePinCmd { pub pin_order: i32, } impl IssueService { #[tracing::instrument(name = "issues.update_pin", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, pin_id = %pin_id))] pub async fn update_pin( &self, caller: i64, space_name: &str, board_id: Uuid, pin_id: Uuid, cmd: UpdatePinCmd, ) -> Result { let space = space_context(self, caller, space_name).await?; authz::require_board_manage(self, &space, caller).await?; let row = issue_pin::Entity::find_by_id(pin_id) .filter(issue_pin::Column::CollectionId.eq(board_id)) .one(self.database.write()) .await? .ok_or(IssueError::NotFound)?; if cmd.pin_order < 0 { return Err(IssueError::BadRequest("pin_order must be >= 0".to_string())); } let mut am: issue_pin::ActiveModel = row.into(); am.pin_order = Set(cmd.pin_order); let updated = am.update(self.database.write()).await?; Ok(PinDto::from(updated)) } } impl IssueService { #[tracing::instrument(name = "issues.create_pin", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id))] pub async fn create_pin( &self, caller: i64, space_name: &str, board_id: Uuid, cmd: CreatePinCmd, ) -> Result { let space = space_context(self, caller, space_name).await?; authz::require_board_manage(self, &space, caller).await?; let board = lookup_board(self, &space, board_id, caller).await?; let _ = lookup_issue(self, &board, cmd.issue_id).await?; if cmd.pin_order < 0 { return Err(IssueError::BadRequest( "pin_order must be non-negative".to_string(), )); } let existing = issue_pin::Entity::find() .filter(issue_pin::Column::CollectionId.eq(board.id)) .filter(issue_pin::Column::IssueId.eq(Some(cmd.issue_id))) .one(self.database.read()) .await?; if existing.is_some() { return Err(IssueError::Conflict("issue already pinned".to_string())); } let now = OffsetDateTime::now_utc(); let row = issue_pin::ActiveModel { id: Set(Uuid::now_v7()), collection_id: Set(board.id), issue_id: Set(Some(cmd.issue_id)), project_id: Set(None), pin_order: Set(cmd.pin_order), created_at: Set(now), }; let inserted = row.insert(self.database.write()).await?; Ok(PinDto::from(inserted)) } pub async fn list_pins( &self, caller: i64, space_name: &str, board_id: Uuid, ) -> Result, IssueError> { let (_, board) = board_context(self, caller, space_name, board_id).await?; let rows = issue_pin::Entity::find() .filter(issue_pin::Column::CollectionId.eq(board.id)) .order_by_asc(issue_pin::Column::PinOrder) .order_by_asc(issue_pin::Column::CreatedAt) .all(self.database.read()) .await?; Ok(rows.into_iter().map(PinDto::from).collect()) } #[tracing::instrument(name = "issues.delete_pin", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, pin_id = %pin_id))] pub async fn delete_pin( &self, caller: i64, space_name: &str, board_id: Uuid, pin_id: Uuid, ) -> Result<(), IssueError> { let space = space_context(self, caller, space_name).await?; authz::require_board_manage(self, &space, caller).await?; let board = lookup_board(self, &space, board_id, caller).await?; let p = issue_pin::Entity::find_by_id(pin_id) .filter(issue_pin::Column::CollectionId.eq(board.id)) .one(self.database.read()) .await? .ok_or(IssueError::NotFound)?; issue_pin::Entity::delete_by_id(p.id) .exec(self.database.write()) .await?; Ok(()) } }