From ac5bb1d02259d2d41e78eab84b20669a1fcc7b2c Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Thu, 3 Jul 2025 20:24:07 +0800 Subject: [PATCH] feat(UI):refactor issue/mr list API --- Cargo.toml | 2 +- jupiter/Cargo.toml | 1 + .../callisto/src/entity_ext/item_assignees.rs | 36 ++++ .../src/entity_ext/mega_conversation.rs | 36 ++++ jupiter/callisto/src/entity_ext/mega_issue.rs | 34 +++- jupiter/callisto/src/entity_ext/mega_mr.rs | 26 ++- jupiter/callisto/src/entity_ext/mod.rs | 6 +- jupiter/callisto/src/item_assignees.rs | 21 ++ jupiter/callisto/src/mega_issue.rs | 2 +- jupiter/callisto/src/mod.rs | 1 + jupiter/callisto/src/prelude.rs | 1 + jupiter/callisto/src/sea_orm_active_enums.rs | 2 + .../m20250702_072055_add_item_assignees.rs | 80 ++++++++ jupiter/src/migration/mod.rs | 2 + jupiter/src/storage.rs | 8 +- jupiter/src/storage/issue_storage.rs | 187 +++++++++++++++--- jupiter/src/storage/mr_storage.rs | 82 ++++++-- jupiter/src/storage/stg_common/item.rs | 34 ++++ jupiter/src/storage/stg_common/mod.rs | 150 ++++++++++++++ jupiter/src/storage/stg_common/model.rs | 29 +++ jupiter/src/storage/stg_common/query_build.rs | 41 ++++ mono/src/api/api_common/label_assignee.rs | 89 +++++++++ mono/src/api/api_common/mod.rs | 2 + mono/src/api/api_common/model.rs | 41 ++++ mono/src/api/issue/issue_router.rs | 101 +++++----- mono/src/api/issue/mod.rs | 59 +++--- mono/src/api/mod.rs | 1 + mono/src/api/mr/mod.rs | 34 +--- mono/src/api/mr/mr_router.rs | 59 +++--- moon/packages/types/generated.ts | 158 +++++++++------ vault/src/integration/vault_core.rs | 5 +- 31 files changed, 1077 insertions(+), 253 deletions(-) create mode 100644 jupiter/callisto/src/entity_ext/item_assignees.rs create mode 100644 jupiter/callisto/src/entity_ext/mega_conversation.rs create mode 100644 jupiter/callisto/src/item_assignees.rs create mode 100644 jupiter/src/migration/m20250702_072055_add_item_assignees.rs create mode 100644 jupiter/src/storage/stg_common/item.rs create mode 100644 jupiter/src/storage/stg_common/mod.rs create mode 100644 jupiter/src/storage/stg_common/model.rs create mode 100644 jupiter/src/storage/stg_common/query_build.rs create mode 100644 mono/src/api/api_common/label_assignee.rs create mode 100644 mono/src/api/api_common/mod.rs create mode 100644 mono/src/api/api_common/model.rs diff --git a/Cargo.toml b/Cargo.toml index bcbf95821..026359e44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,7 +145,7 @@ qdrant-client = "1.7" walkdir = "2.4" dagrs = "0.5.0" tar = "0.4" - +indexmap = "2.10.0" envsubst = "0.2.1" rcgen = "0.13.2" diffs = "0.5.1" diff --git a/jupiter/Cargo.toml b/jupiter/Cargo.toml index 3e97c5f1d..40f12288b 100644 --- a/jupiter/Cargo.toml +++ b/jupiter/Cargo.toml @@ -40,3 +40,4 @@ aws-config = { workspace = true, features = ["behavior-version-latest"] } aws-sdk-s3 = { workspace = true, features = ["rt-tokio"] } anyhow = { workspace = true } tempfile = { workspace = true } +indexmap = { workspace = true } diff --git a/jupiter/callisto/src/entity_ext/item_assignees.rs b/jupiter/callisto/src/entity_ext/item_assignees.rs new file mode 100644 index 000000000..5e6a85d0a --- /dev/null +++ b/jupiter/callisto/src/entity_ext/item_assignees.rs @@ -0,0 +1,36 @@ +use sea_orm::entity::prelude::*; + +use crate::{item_assignees::Column, item_assignees::Entity}; + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + MegaIssue, + MegaMr, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::MegaIssue => Entity::belongs_to(crate::mega_issue::Entity) + .from(Column::ItemId) + .to(crate::mega_issue::Column::Id) + .into(), + Self::MegaMr => Entity::belongs_to(crate::mega_mr::Entity) + .from(Column::ItemId) + .to(crate::mega_mr::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaIssue.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaMr.def() + } +} diff --git a/jupiter/callisto/src/entity_ext/mega_conversation.rs b/jupiter/callisto/src/entity_ext/mega_conversation.rs new file mode 100644 index 000000000..4da288880 --- /dev/null +++ b/jupiter/callisto/src/entity_ext/mega_conversation.rs @@ -0,0 +1,36 @@ +use sea_orm::entity::prelude::*; + +use crate::{mega_conversation::Column, mega_conversation::Entity}; + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + MegaIssue, + MegaMr, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::MegaIssue => Entity::belongs_to(crate::mega_issue::Entity) + .from(Column::Link) + .to(crate::mega_issue::Column::Link) + .into(), + Self::MegaMr => Entity::belongs_to(crate::mega_mr::Entity) + .from(Column::Link) + .to(crate::mega_mr::Column::Link) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaIssue.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaMr.def() + } +} diff --git a/jupiter/callisto/src/entity_ext/mega_issue.rs b/jupiter/callisto/src/entity_ext/mega_issue.rs index 9cf738629..2037e7e28 100644 --- a/jupiter/callisto/src/entity_ext/mega_issue.rs +++ b/jupiter/callisto/src/entity_ext/mega_issue.rs @@ -4,13 +4,17 @@ use crate::mega_issue::Entity; #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { - Label, + ItemLabels, + ItemAssignees, + Conversation, } impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Label => Entity::has_many(crate::item_labels::Entity).into(), + Self::ItemLabels => Entity::has_many(crate::item_labels::Entity).into(), + Self::ItemAssignees => Entity::has_many(crate::item_assignees::Entity).into(), + Self::Conversation => Entity::has_many(crate::mega_conversation::Entity).into(), } } } @@ -21,6 +25,28 @@ impl Related for Entity { } fn via() -> Option { - Some(crate::entity_ext::item_labels::Relation::MegaIssue.def().rev()) + Some( + crate::entity_ext::item_labels::Relation::MegaIssue + .def() + .rev(), + ) } -} \ No newline at end of file +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ItemAssignees.def() + } + fn via() -> Option { + None + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Conversation.def() + } + fn via() -> Option { + None + } +} diff --git a/jupiter/callisto/src/entity_ext/mega_mr.rs b/jupiter/callisto/src/entity_ext/mega_mr.rs index 03b3cb6c4..41ac69a9b 100644 --- a/jupiter/callisto/src/entity_ext/mega_mr.rs +++ b/jupiter/callisto/src/entity_ext/mega_mr.rs @@ -4,13 +4,17 @@ use crate::mega_mr::Entity; #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { - Label, + ItemLabels, + ItemAssignees, + Conversation, } impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Label => Entity::has_many(crate::item_labels::Entity).into(), + Self::ItemLabels => Entity::has_many(crate::item_labels::Entity).into(), + Self::ItemAssignees => Entity::has_many(crate::item_assignees::Entity).into(), + Self::Conversation => Entity::has_many(crate::mega_conversation::Entity).into(), } } } @@ -24,3 +28,21 @@ impl Related for Entity { Some(crate::entity_ext::item_labels::Relation::MegaMr.def().rev()) } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ItemAssignees.def() + } + fn via() -> Option { + None + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Conversation.def() + } + fn via() -> Option { + None + } +} diff --git a/jupiter/callisto/src/entity_ext/mod.rs b/jupiter/callisto/src/entity_ext/mod.rs index 1c6c17f11..5845e5d38 100644 --- a/jupiter/callisto/src/entity_ext/mod.rs +++ b/jupiter/callisto/src/entity_ext/mod.rs @@ -1,4 +1,6 @@ +pub mod item_assignees; +pub mod item_labels; +pub mod label; pub mod mega_issue; pub mod mega_mr; -pub mod item_labels; -pub mod label; \ No newline at end of file +pub mod mega_conversation; \ No newline at end of file diff --git a/jupiter/callisto/src/item_assignees.rs b/jupiter/callisto/src/item_assignees.rs new file mode 100644 index 000000000..560b3a407 --- /dev/null +++ b/jupiter/callisto/src/item_assignees.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.12 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "item_assignees")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key, auto_increment = false)] + pub item_id: i64, + #[sea_orm(primary_key, auto_increment = false)] + pub assignnee_id: String, + pub item_type: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mega_issue.rs b/jupiter/callisto/src/mega_issue.rs index 56d9f26b6..995e88ecf 100644 --- a/jupiter/callisto/src/mega_issue.rs +++ b/jupiter/callisto/src/mega_issue.rs @@ -15,7 +15,7 @@ pub struct Model { pub created_at: DateTime, pub updated_at: DateTime, pub closed_at: Option, - pub user_id: String, + pub author: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/jupiter/callisto/src/mod.rs b/jupiter/callisto/src/mod.rs index 0284fe6c7..43b08847a 100644 --- a/jupiter/callisto/src/mod.rs +++ b/jupiter/callisto/src/mod.rs @@ -13,6 +13,7 @@ pub mod git_repo; pub mod git_tag; pub mod git_tree; pub mod import_refs; +pub mod item_assignees; pub mod item_labels; pub mod label; pub mod lfs_locks; diff --git a/jupiter/callisto/src/prelude.rs b/jupiter/callisto/src/prelude.rs index 6959f4cba..6e3c76103 100644 --- a/jupiter/callisto/src/prelude.rs +++ b/jupiter/callisto/src/prelude.rs @@ -10,6 +10,7 @@ pub use super::git_repo::Entity as GitRepo; pub use super::git_tag::Entity as GitTag; pub use super::git_tree::Entity as GitTree; pub use super::import_refs::Entity as ImportRefs; +pub use super::item_assignees::Entity as ItemAssignees; pub use super::item_labels::Entity as ItemLabels; pub use super::label::Entity as Label; pub use super::lfs_locks::Entity as LfsLocks; diff --git a/jupiter/callisto/src/sea_orm_active_enums.rs b/jupiter/callisto/src/sea_orm_active_enums.rs index 92f27feb5..92c4ff96f 100644 --- a/jupiter/callisto/src/sea_orm_active_enums.rs +++ b/jupiter/callisto/src/sea_orm_active_enums.rs @@ -33,6 +33,8 @@ pub enum ConvTypeEnum { Reopen, #[sea_orm(string_value = "label")] Label, + #[sea_orm(string_value = "assignee")] + Assignee, } #[derive( Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema, diff --git a/jupiter/src/migration/m20250702_072055_add_item_assignees.rs b/jupiter/src/migration/m20250702_072055_add_item_assignees.rs new file mode 100644 index 000000000..ff68e5245 --- /dev/null +++ b/jupiter/src/migration/m20250702_072055_add_item_assignees.rs @@ -0,0 +1,80 @@ +use sea_orm::DatabaseBackend; +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let backend = manager.get_database_backend(); + + match backend { + DatabaseBackend::Postgres => { + manager + .get_connection() + .execute_unprepared( + r#"ALTER TYPE conv_type_enum ADD VALUE IF NOT EXISTS 'assignee';"#, + ) + .await?; + } + DatabaseBackend::Sqlite | DatabaseBackend::MySql => {} + } + + manager + .create_table( + table_auto(ItemAssignees::Table) + .col(big_integer(ItemAssignees::ItemId)) + .col(string(ItemAssignees::AssignneeId)) + .col(string(ItemAssignees::ItemType)) + .primary_key( + Index::create() + .col(ItemAssignees::ItemId) + .col(ItemAssignees::AssignneeId), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(MegaIssue::Table) + .drop_column("user_id") + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(MegaIssue::Table) + .add_column_if_not_exists( + ColumnDef::new(Alias::new("author")) + .string() + .not_null() + .default(""), + ) + .to_owned(), + ) + .await?; + Ok(()) + } + + async fn down(&self, _: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} + +#[derive(DeriveIden)] +enum ItemAssignees { + Table, + ItemId, + AssignneeId, + ItemType, +} + +#[derive(DeriveIden)] +enum MegaIssue { + Table, +} diff --git a/jupiter/src/migration/mod.rs b/jupiter/src/migration/mod.rs index c92d857db..2f33fce97 100644 --- a/jupiter/src/migration/mod.rs +++ b/jupiter/src/migration/mod.rs @@ -42,6 +42,7 @@ mod m20250610_000001_add_vault_storage; mod m20250613_033821_alter_user_id; mod m20250618_065050_add_label; mod m20250628_025312_add_username_in_conversation; +mod m20250702_072055_add_item_assignees; /// Creates a primary key column definition with big integer type. /// @@ -72,6 +73,7 @@ impl MigratorTrait for Migrator { Box::new(m20250613_033821_alter_user_id::Migration), Box::new(m20250618_065050_add_label::Migration), Box::new(m20250628_025312_add_username_in_conversation::Migration), + Box::new(m20250702_072055_add_item_assignees::Migration), ] } } diff --git a/jupiter/src/storage.rs b/jupiter/src/storage.rs index 80e1a1568..4bbd7360f 100644 --- a/jupiter/src/storage.rs +++ b/jupiter/src/storage.rs @@ -7,16 +7,16 @@ pub mod mq_storage; pub mod mr_storage; pub mod raw_db_storage; pub mod relay_storage; +pub mod stg_common; pub mod user_storage; pub mod vault_storage; -use sea_orm::{sea_query::OnConflict, ActiveModelTrait, ConnectionTrait, DbErr, EntityTrait}; - -use common::errors::MegaError; - use std::sync::{Arc, LazyLock, Weak}; +use sea_orm::{sea_query::OnConflict, ActiveModelTrait, ConnectionTrait, DbErr, EntityTrait}; + use common::config::Config; +use common::errors::MegaError; use crate::lfs_storage::{self, local_storage::LocalStorage, LfsFileStorage}; use crate::storage::init::database_connection; diff --git a/jupiter/src/storage/issue_storage.rs b/jupiter/src/storage/issue_storage.rs index 67ba6471a..cdfb09226 100644 --- a/jupiter/src/storage/issue_storage.rs +++ b/jupiter/src/storage/issue_storage.rs @@ -1,15 +1,21 @@ +use std::collections::HashMap; +use std::sync::Arc; + use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, - PaginatorTrait, QueryFilter, QueryOrder, Set, TransactionTrait, + ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait, IntoActiveModel, + JoinType, PaginatorTrait, QueryFilter, QuerySelect, RelationTrait, Set, TransactionTrait, }; -use std::sync::Arc; use callisto::sea_orm_active_enums::ConvTypeEnum; -use callisto::{item_labels, label, mega_conversation, mega_issue}; +use callisto::{item_assignees, item_labels, label, mega_conversation, mega_issue}; use common::errors::MegaError; use common::model::Pagination; use common::utils::{generate_id, generate_link}; +use crate::storage::stg_common::combine_item_list; +use crate::storage::stg_common::model::{ItemDetails, LabelAssigneeParams, ListParams}; +use crate::storage::stg_common::query_build::{apply_sort, filter_by_assignees, filter_by_labels}; + #[derive(Clone)] pub struct IssueStorage { pub connection: Arc, @@ -30,32 +36,70 @@ impl IssueStorage { } } - pub async fn get_issue_by_status( + pub async fn get_issue_list( &self, - status: &str, + params: ListParams, page: Pagination, - ) -> Result<(Vec<(mega_issue::Model, Vec)>, u64), MegaError> { - let paginator = mega_issue::Entity::find() - .filter(mega_issue::Column::Status.eq(status)) - .order_by_desc(mega_issue::Column::CreatedAt) - .paginate(self.get_connection(), page.per_page); + ) -> Result<(Vec, u64), MegaError> { + let cond = Condition::all(); + let cond = filter_by_labels(cond, params.labels); + let cond = filter_by_author(cond, params.author); + let cond = filter_by_assignees(cond, params.assignees); + + let query = mega_issue::Entity::find() + .join( + JoinType::LeftJoin, + callisto::entity_ext::mega_issue::Relation::ItemLabels.def(), + ) + .filter(mega_issue::Column::Status.eq(params.status)) + .filter(cond) + .distinct(); + + let mut sort_map = HashMap::new(); + sort_map.insert("created_at", mega_issue::Column::CreatedAt); + sort_map.insert("updated_at", mega_issue::Column::UpdatedAt); + + let query = apply_sort(query, params.sort_by.as_deref(), params.asc, &sort_map); + + let paginator = query.paginate(self.get_connection(), page.per_page); let num_pages = paginator.num_items().await?; let (issues, page) = paginator .fetch_page(page.page - 1) .await .map(|m| (m, num_pages))?; - let issues_with_label: Vec<(mega_issue::Model, Vec)> = + let issue_ids = issues.iter().map(|m| m.id).collect::>(); + + let label_query = + mega_issue::Entity::find().filter(mega_issue::Column::Id.is_in(issue_ids.clone())); + let label_query = apply_sort( + label_query, + params.sort_by.as_deref(), + params.asc, + &sort_map, + ); + let labels: Vec<(mega_issue::Model, Vec)> = label_query + .find_with_related(label::Entity) + .all(self.get_connection()) + .await?; + + let assignees: Vec<(mega_issue::Model, Vec)> = + mega_issue::Entity::find() + .filter(mega_issue::Column::Id.is_in(issue_ids.clone())) + .find_with_related(item_assignees::Entity) + .all(self.get_connection()) + .await?; + + let conversations: Vec<(mega_issue::Model, Vec)> = mega_issue::Entity::find() - .filter( - mega_issue::Column::Id.is_in(issues.iter().map(|i| i.id).collect::>()), - ) - .order_by_desc(mega_issue::Column::CreatedAt) - .find_with_related(label::Entity) + .filter(mega_issue::Column::Id.is_in(issue_ids)) + .find_with_related(mega_conversation::Entity) .all(self.get_connection()) .await?; - Ok((issues_with_label, page)) + let res = combine_item_list::(labels, assignees, conversations); + + Ok((res, page)) } pub async fn get_issue(&self, link: &str) -> Result, MegaError> { @@ -77,14 +121,14 @@ impl IssueStorage { pub async fn save_issue( &self, - user_id: &str, + username: &str, title: &str, ) -> Result { let model = mega_issue::Model { id: generate_id(), link: generate_link(), title: title.to_owned(), - user_id: user_id.to_owned(), + author: username.to_owned(), status: "open".to_owned(), created_at: chrono::Utc::now().naive_utc(), updated_at: chrono::Utc::now().naive_utc(), @@ -199,16 +243,32 @@ impl IssueStorage { Ok(item_labels) } - pub async fn modify_labels( + pub async fn find_item_exist_assignees( &self, - username: &str, item_id: i64, - link: &str, + ) -> Result, MegaError> { + let item_assignees = item_assignees::Entity::find() + .filter(item_assignees::Column::ItemId.eq(item_id)) + .all(self.get_connection()) + .await?; + Ok(item_assignees) + } + + pub async fn modify_labels( + &self, to_add: Vec, to_remove: Vec, + params: LabelAssigneeParams, ) -> Result<(), MegaError> { let txn = self.get_connection().begin().await?; + let LabelAssigneeParams { + item_id, + link, + username, + item_type, + } = params; + if !to_remove.is_empty() { item_labels::Entity::delete_many() .filter(item_labels::Column::ItemId.eq(item_id)) @@ -217,8 +277,8 @@ impl IssueStorage { .await?; self.add_conversation( - link, - username, + &link, + &username, Some(format!("{username} removed {to_remove:?}")), ConvTypeEnum::Label, ) @@ -234,7 +294,7 @@ impl IssueStorage { updated_at: chrono::Utc::now().naive_utc(), item_id, label_id, - item_type: String::from("issue"), + item_type: item_type.clone(), } .into_active_model(), ); @@ -244,8 +304,8 @@ impl IssueStorage { .exec(&txn) .await?; self.add_conversation( - link, - username, + &link, + &username, Some(format!("{username} added {to_add:?}")), ConvTypeEnum::Label, ) @@ -256,4 +316,75 @@ impl IssueStorage { Ok(()) } + + pub async fn modify_assignees( + &self, + to_add: Vec, + to_remove: Vec, + params: LabelAssigneeParams, + ) -> Result<(), MegaError> { + let txn = self.get_connection().begin().await?; + + let LabelAssigneeParams { + item_id, + link, + username, + item_type, + } = params; + + if !to_remove.is_empty() { + item_assignees::Entity::delete_many() + .filter(item_assignees::Column::ItemId.eq(item_id)) + .filter(item_assignees::Column::AssignneeId.is_in(to_remove.clone())) + .exec(&txn) + .await?; + + self.add_conversation( + &link, + &username, + Some(format!("{username} unassigned {to_remove:?}")), + ConvTypeEnum::Assignee, + ) + .await?; + } + + if !to_add.is_empty() { + let mut new_item = Vec::new(); + for assignnee_id in to_add.clone() { + new_item.push( + item_assignees::Model { + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + item_id, + assignnee_id, + item_type: item_type.clone(), + } + .into_active_model(), + ); + } + + item_assignees::Entity::insert_many(new_item) + .exec(&txn) + .await?; + self.add_conversation( + &link, + &username, + Some(format!("{username} assigned {to_add:?}")), + ConvTypeEnum::Assignee, + ) + .await?; + } + + txn.commit().await?; + + Ok(()) + } +} + +fn filter_by_author(cond: Condition, author: Option) -> Condition { + if let Some(value) = author { + cond.add(mega_issue::Column::Author.eq(value)) + } else { + cond + } } diff --git a/jupiter/src/storage/mr_storage.rs b/jupiter/src/storage/mr_storage.rs index 8977c734c..09411866a 100644 --- a/jupiter/src/storage/mr_storage.rs +++ b/jupiter/src/storage/mr_storage.rs @@ -1,16 +1,21 @@ +use std::collections::HashMap; use std::sync::Arc; use common::model::Pagination; use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, - PaginatorTrait, QueryFilter, QueryOrder, Set, + ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait, IntoActiveModel, + JoinType, PaginatorTrait, QueryFilter, QuerySelect, RelationTrait, Set, }; use callisto::sea_orm_active_enums::MergeStatusEnum; -use callisto::{label, mega_mr}; +use callisto::{item_assignees, label, mega_conversation, mega_mr}; use common::errors::MegaError; use common::utils::generate_id; +use crate::storage::stg_common::combine_item_list; +use crate::storage::stg_common::model::{ItemDetails, ListParams}; +use crate::storage::stg_common::query_build::{apply_sort, filter_by_assignees, filter_by_labels}; + #[derive(Clone)] pub struct MrStorage { pub connection: Arc, @@ -44,15 +49,43 @@ impl MrStorage { Ok(model) } - pub async fn get_mr_by_status( + pub async fn get_mr_list( &self, - status: Vec, + params: ListParams, page: Pagination, - ) -> Result<(Vec<(mega_mr::Model, Vec)>, u64), MegaError> { - let paginator = mega_mr::Entity::find() + ) -> Result<(Vec, u64), MegaError> { + let cond = Condition::all(); + let cond = filter_by_labels(cond, params.labels); + // let cond = filter_by_author(cond, params.author); + let cond = filter_by_assignees(cond, params.assignees); + + let status = if params.status == "open" { + vec![MergeStatusEnum::Open] + } else if params.status == "closed" { + vec![MergeStatusEnum::Closed, MergeStatusEnum::Merged] + } else { + vec![ + MergeStatusEnum::Open, + MergeStatusEnum::Closed, + MergeStatusEnum::Merged, + ] + }; + + let query = mega_mr::Entity::find() + .join( + JoinType::LeftJoin, + callisto::entity_ext::mega_mr::Relation::ItemLabels.def(), + ) .filter(mega_mr::Column::Status.is_in(status)) - .order_by_desc(mega_mr::Column::CreatedAt) - .paginate(self.get_connection(), page.per_page); + .filter(cond) + .distinct(); + + let mut sort_map = HashMap::new(); + sort_map.insert("created_at", mega_mr::Column::CreatedAt); + sort_map.insert("updated_at", mega_mr::Column::UpdatedAt); + + let query = apply_sort(query, params.sort_by.as_deref(), params.asc, &sort_map); + let paginator = query.paginate(self.get_connection(), page.per_page); let num_pages = paginator.num_items().await?; let (mr_list, page) = paginator @@ -60,13 +93,36 @@ impl MrStorage { .await .map(|m| (m, num_pages))?; - let mr_with_label: Vec<(mega_mr::Model, Vec)> = mega_mr::Entity::find() - .filter(mega_mr::Column::Id.is_in(mr_list.iter().map(|i| i.id).collect::>())) - .order_by_desc(mega_mr::Column::CreatedAt) + let ids = mr_list.iter().map(|m| m.id).collect::>(); + + let label_query = mega_mr::Entity::find().filter(mega_mr::Column::Id.is_in(ids.clone())); + let label_query = apply_sort( + label_query, + params.sort_by.as_deref(), + params.asc, + &sort_map, + ); + let labels: Vec<(mega_mr::Model, Vec)> = label_query .find_with_related(label::Entity) .all(self.get_connection()) .await?; - Ok((mr_with_label, page)) + + let assignees: Vec<(mega_mr::Model, Vec)> = mega_mr::Entity::find() + .filter(mega_mr::Column::Id.is_in(ids.clone())) + .find_with_related(item_assignees::Entity) + .all(self.get_connection()) + .await?; + + let conversations: Vec<(mega_mr::Model, Vec)> = + mega_mr::Entity::find() + .filter(mega_mr::Column::Id.is_in(ids)) + .find_with_related(mega_conversation::Entity) + .all(self.get_connection()) + .await?; + + let res = combine_item_list::(labels, assignees, conversations); + + Ok((res, page)) } pub async fn get_mr(&self, link: &str) -> Result, MegaError> { diff --git a/jupiter/src/storage/stg_common/item.rs b/jupiter/src/storage/stg_common/item.rs new file mode 100644 index 000000000..2ff6ab062 --- /dev/null +++ b/jupiter/src/storage/stg_common/item.rs @@ -0,0 +1,34 @@ +use callisto::{mega_issue, mega_mr}; + +use crate::storage::stg_common::model::ItemKind; + +pub trait ItemEntity { + type Model; + fn item_kind(model: Self::Model) -> ItemKind; + + fn get_id(model: &Self::Model) -> i64; +} + +impl ItemEntity for mega_issue::Entity { + type Model = mega_issue::Model; + + fn item_kind(model: Self::Model) -> ItemKind { + ItemKind::Issue(model) + } + + fn get_id(model: &Self::Model) -> i64 { + model.id + } +} + +impl ItemEntity for mega_mr::Entity { + type Model = mega_mr::Model; + + fn item_kind(model: Self::Model) -> ItemKind { + ItemKind::Mr(model) + } + + fn get_id(model: &Self::Model) -> i64 { + model.id + } +} diff --git a/jupiter/src/storage/stg_common/mod.rs b/jupiter/src/storage/stg_common/mod.rs new file mode 100644 index 000000000..b31a48179 --- /dev/null +++ b/jupiter/src/storage/stg_common/mod.rs @@ -0,0 +1,150 @@ +use std::collections::HashMap; + +use indexmap::IndexMap; + +use callisto::{item_assignees, label, mega_conversation, sea_orm_active_enums::ConvTypeEnum}; + +use crate::storage::stg_common::{item::ItemEntity, model::ItemDetails}; + +pub mod item; +pub mod model; +pub mod query_build; + +/// Combine labels, assignees, and conversations into a unified list of `ItemDetails`. +/// +/// This function merges multiple related datasets for a list of issues: +/// - `item_labels`: pairs of (issue, list of labels) +/// - `item_assignees`: pairs of (issue, list of assignees) +/// - `conversations`: pairs of (issue, list of conversations) +/// +/// It aggregates the data into a single `ItemDetails` structure for each issue, +/// ensuring that even if some parts are missing (e.g. labels or assignees), +/// a complete `ItemDetails` entry is still created. +/// +/// # Arguments +/// +/// * `item_labels` - A vector of tuples where each tuple contains an issue and its associated labels. +/// * `item_assignees` - A vector of tuples where each tuple contains an issue and its associated assignees. +/// * `conversations` - A vector of tuples where each tuple contains an issue and its associated conversations. +/// +/// # Returns +/// +/// A vector of `ItemDetails` combining the data from the above sources. +pub fn combine_item_list( + item_labels: Vec<(T::Model, Vec)>, + item_assignees: Vec<(T::Model, Vec)>, + conversations: Vec<(T::Model, Vec)>, +) -> Vec +where + T: ItemEntity, + T::Model: Clone, +{ + let mut conv_map = HashMap::new(); + for (model, convs) in conversations { + let id = T::get_id(&model); + conv_map.insert( + id, + convs + .into_iter() + .filter(|m| m.conv_type == ConvTypeEnum::Comment) + .collect::>() + .len(), + ); + } + + let mut result: IndexMap = IndexMap::new(); + for (model, labels) in item_labels { + let id = T::get_id(&model); + result.insert( + id, + ItemDetails { + item: T::item_kind(model), + labels, + assignees: vec![], + comment_num: *conv_map.get(&id).unwrap_or(&0), + }, + ); + } + + for (model, assignees) in item_assignees { + let id = T::get_id(&model); + let assignees = assignees.iter().map(|m| m.assignnee_id.clone()).collect(); + if let Some(entry) = result.get_mut(&id) { + entry.assignees = assignees; + } else { + result.insert( + id, + ItemDetails { + item: T::item_kind(model), + labels: vec![], + assignees, + comment_num: *conv_map.get(&id).unwrap_or(&0), + }, + ); + } + } + + result.into_values().collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use callisto::{ + item_assignees, label, mega_conversation, mega_issue, sea_orm_active_enums::ConvTypeEnum, + }; + + #[test] + fn test_combine_item_list() { + let issue = mega_issue::Model { + id: 1, + link: String::from("ILD2EV5V"), + title: String::from("[Monobean] no such column: mega_refs.is_mr #1028"), + status: String::from("open"), + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + closed_at: None, + author: String::from("benjamin_747"), + }; + + let label = label::Model { + id: 1, + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + name: String::from("bugs"), + color: String::from("#000000"), + description: String::from("des"), + }; + + let assignee = item_assignees::Model { + item_id: 1, + assignnee_id: "alice".to_string(), + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + item_type: String::from("issue"), + }; + + let conv = mega_conversation::Model { + id: 1, + conv_type: ConvTypeEnum::Comment, + link: String::from("ILD2EV5V"), + comment: None, + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + username: String::from("benjamin_747"), + }; + + let item_labels = vec![(issue.clone(), vec![label])]; + let item_assignees = vec![(issue.clone(), vec![assignee])]; + let conversations = vec![(issue.clone(), vec![conv])]; + + let results = + combine_item_list::(item_labels, item_assignees, conversations); + + assert_eq!(results.len(), 1); + let details = &results[0]; + assert_eq!(details.comment_num, 1); + assert_eq!(details.labels.len(), 1); + assert_eq!(details.assignees, vec!["alice"]); + } +} diff --git a/jupiter/src/storage/stg_common/model.rs b/jupiter/src/storage/stg_common/model.rs new file mode 100644 index 000000000..9737b1c9d --- /dev/null +++ b/jupiter/src/storage/stg_common/model.rs @@ -0,0 +1,29 @@ +use callisto::{label, mega_issue, mega_mr}; + +pub enum ItemKind { + Issue(mega_issue::Model), + Mr(mega_mr::Model), +} + +pub struct ItemDetails { + pub item: ItemKind, + pub labels: Vec, + pub assignees: Vec, + pub comment_num: usize, +} + +pub struct ListParams { + pub status: String, + pub author: Option, + pub labels: Option>, + pub assignees: Option>, + pub sort_by: Option, + pub asc: bool, +} + +pub struct LabelAssigneeParams { + pub item_id: i64, + pub link: String, + pub username: String, + pub item_type: String, +} diff --git a/jupiter/src/storage/stg_common/query_build.rs b/jupiter/src/storage/stg_common/query_build.rs new file mode 100644 index 000000000..97f373c3b --- /dev/null +++ b/jupiter/src/storage/stg_common/query_build.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; + +use callisto::{item_assignees, item_labels}; + +use sea_orm::{ColumnTrait, Condition, Order, QueryOrder}; + +pub fn filter_by_labels(cond: Condition, labels: Option>) -> Condition { + if let Some(value) = labels { + cond.add(item_labels::Column::LabelId.is_in(value)) + } else { + cond + } +} + +pub fn filter_by_assignees(cond: Condition, assignees: Option>) -> Condition { + if let Some(value) = assignees { + cond.add(item_assignees::Column::AssignneeId.is_in(value)) + } else { + cond + } +} + +/// Apply order_by dynamically based on user input. +pub fn apply_sort( + mut query: Q, + sort_by: Option<&str>, + asc: bool, + columns: &HashMap<&str, C>, +) -> Q +where + C: ColumnTrait + Copy, + Q: QueryOrder + Sized, +{ + if let Some(field) = sort_by { + if let Some(column) = columns.get(field) { + let order = if asc { Order::Asc } else { Order::Desc }; + query = query.order_by(*column, order); + } + } + query +} diff --git a/mono/src/api/api_common/label_assignee.rs b/mono/src/api/api_common/label_assignee.rs new file mode 100644 index 000000000..36f812931 --- /dev/null +++ b/mono/src/api/api_common/label_assignee.rs @@ -0,0 +1,89 @@ +use std::collections::HashSet; + +use axum::{extract::State, Json}; + +use common::model::CommonResult; +use jupiter::storage::stg_common::model::LabelAssigneeParams; + +use crate::api::error::ApiError; +use crate::api::MonoApiServiceState; +use crate::api::{ + api_common::model::{AssigneeUpdatePayload, LabelUpdatePayload}, + oauth::model::LoginUser, +}; + +pub async fn label_update( + user: LoginUser, + state: State, + payload: LabelUpdatePayload, + item_type: String, +) -> Result>, ApiError> { + let issue_storage = state.issue_stg(); + + let LabelUpdatePayload { + label_ids, + link, + item_id, + } = payload; + + let old_labels = issue_storage + .find_item_exist_labels(payload.item_id) + .await + .unwrap(); + + let old_ids: HashSet = old_labels.iter().map(|l| l.label_id).collect(); + let new_ids: HashSet = label_ids.iter().copied().collect(); + + let to_add: Vec = new_ids.difference(&old_ids).copied().collect(); + let to_remove: Vec = old_ids.difference(&new_ids).copied().collect(); + + let params = LabelAssigneeParams { + item_id, + link, + username: user.username, + item_type, + }; + + issue_storage + .modify_labels(to_add, to_remove, params) + .await?; + Ok(Json(CommonResult::success(None))) +} + +pub async fn assignees_update( + user: LoginUser, + state: State, + payload: AssigneeUpdatePayload, + item_type: String, +) -> Result>, ApiError> { + let issue_storage = state.issue_stg(); + + let AssigneeUpdatePayload { + assignees, + link, + item_id, + } = payload; + + let old_models = issue_storage + .find_item_exist_assignees(payload.item_id) + .await + .unwrap(); + + let old_ids: HashSet = old_models.iter().map(|m| m.assignnee_id.clone()).collect(); + let new_ids: HashSet = assignees.iter().cloned().collect(); + + let to_add: Vec = new_ids.difference(&old_ids).cloned().collect(); + let to_remove: Vec = old_ids.difference(&new_ids).cloned().collect(); + + let params = LabelAssigneeParams { + item_id, + link, + username: user.username, + item_type, + }; + + issue_storage + .modify_assignees(to_add, to_remove, params) + .await?; + Ok(Json(CommonResult::success(None))) +} diff --git a/mono/src/api/api_common/mod.rs b/mono/src/api/api_common/mod.rs new file mode 100644 index 000000000..00bf63bfc --- /dev/null +++ b/mono/src/api/api_common/mod.rs @@ -0,0 +1,2 @@ +pub mod label_assignee; +pub mod model; diff --git a/mono/src/api/api_common/model.rs b/mono/src/api/api_common/model.rs new file mode 100644 index 000000000..3306abc05 --- /dev/null +++ b/mono/src/api/api_common/model.rs @@ -0,0 +1,41 @@ +use serde::Deserialize; +use utoipa::ToSchema; + +use jupiter::storage::stg_common::model::ListParams; + +#[derive(Deserialize, ToSchema)] +pub struct LabelUpdatePayload { + pub label_ids: Vec, + pub item_id: i64, + pub link: String, +} + +#[derive(Deserialize, ToSchema)] +pub struct AssigneeUpdatePayload { + pub assignees: Vec, + pub item_id: i64, + pub link: String, +} + +#[derive(Deserialize, ToSchema)] +pub struct ListPayload { + pub status: String, + pub author: Option, + pub labels: Option>, + pub assignees: Option>, + pub sort_by: Option, + pub asc: bool, +} + +impl From for ListParams { + fn from(value: ListPayload) -> Self { + Self { + status: value.status, + author: value.author, + labels: value.labels, + assignees: value.assignees, + sort_by: value.sort_by, + asc: value.asc, + } + } +} diff --git a/mono/src/api/issue/issue_router.rs b/mono/src/api/issue/issue_router.rs index 6ff00628a..c144032e2 100644 --- a/mono/src/api/issue/issue_router.rs +++ b/mono/src/api/issue/issue_router.rs @@ -1,20 +1,22 @@ -use std::collections::HashSet; - use axum::{ extract::{Path, State}, Json, }; -use serde::Deserialize; -use utoipa::ToSchema; use utoipa_axum::{router::OpenApiRouter, routes}; use callisto::sea_orm_active_enums::ConvTypeEnum; use common::model::{CommonPage, CommonResult, PageParams}; -use crate::api::MonoApiServiceState; -use crate::api::{issue::LabelUpdatePayload, mr::SaveCommentRequest}; use crate::api::{ - issue::{IssueDetail, IssueItem, NewIssue}, + api_common::model::{LabelUpdatePayload, ListPayload}, + mr::SaveCommentRequest, +}; +use crate::api::{ + api_common::{self, model::AssigneeUpdatePayload}, + MonoApiServiceState, +}; +use crate::api::{ + issue::{IssueDetail, ItemRes, NewIssue}, oauth::model::LoginUser, }; use crate::{api::error::ApiError, server::https_server::ISSUE_TAG}; @@ -30,32 +32,28 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(issue_detail)) .routes(routes!(save_comment)) .routes(routes!(delete_comment)) - .routes(routes!(labels)), + .routes(routes!(labels)) + .routes(routes!(assignees)), ) } -#[derive(Deserialize, ToSchema)] -pub struct StatusParams { - pub status: String, -} - /// Fetch Issue list #[utoipa::path( post, path = "/list", - request_body = PageParams, + request_body = PageParams, responses( - (status = 200, body = CommonResult>, content_type = "application/json") + (status = 200, body = CommonResult>, content_type = "application/json") ), tag = ISSUE_TAG )] async fn fetch_issue_list( state: State, - Json(json): Json>, -) -> Result>>, ApiError> { + Json(json): Json>, +) -> Result>>, ApiError> { let (items, total) = state .issue_stg() - .get_issue_by_status(&json.additional.status, json.pagination) + .get_issue_list(json.additional.into(), json.pagination) .await?; Ok(Json(CommonResult::success(Some(CommonPage { items: items.into_iter().map(|m| m.into()).collect(), @@ -107,10 +105,7 @@ async fn new_issue( Json(json): Json, ) -> Result>, ApiError> { let stg = state.issue_stg().clone(); - let res = stg - .save_issue(&user.username, &json.title) - .await - .unwrap(); + let res = stg.save_issue(&user.username, &json.title).await.unwrap(); let _ = stg .add_conversation( &res.link, @@ -135,11 +130,20 @@ async fn new_issue( tag = ISSUE_TAG )] async fn close_issue( - _: LoginUser, + user: LoginUser, Path(link): Path, state: State, ) -> Result>, ApiError> { state.issue_stg().close_issue(&link).await?; + state + .issue_stg() + .add_conversation( + &link, + &user.username, + Some(format!("{} closed this", user.username)), + ConvTypeEnum::Closed, + ) + .await?; Ok(Json(CommonResult::success(None))) } @@ -156,11 +160,20 @@ async fn close_issue( tag = ISSUE_TAG )] async fn reopen_issue( - _: LoginUser, + user: LoginUser, Path(link): Path, state: State, ) -> Result>, ApiError> { state.issue_stg().reopen_issue(&link).await?; + state + .issue_stg() + .add_conversation( + &link, + &user.username, + Some(format!("{} reopen this", user.username)), + ConvTypeEnum::Closed, + ) + .await?; Ok(Json(CommonResult::success(None))) } @@ -231,35 +244,23 @@ async fn labels( state: State, Json(payload): Json, ) -> Result>, ApiError> { - common_label_update(user, state, payload).await + api_common::label_assignee::label_update(user, state, payload, String::from("issue")).await } -pub async fn common_label_update( +/// update issue related assignees +#[utoipa::path( + post, + path = "/assignees", + request_body = AssigneeUpdatePayload, + responses( + (status = 200, body = CommonResult, content_type = "application/json") + ), + tag = ISSUE_TAG +)] +async fn assignees( user: LoginUser, state: State, - payload: LabelUpdatePayload, + Json(payload): Json, ) -> Result>, ApiError> { - let issue_storage = state.issue_stg(); - - let LabelUpdatePayload { - label_ids, - link, - item_id, - } = payload; - - let old_labels = issue_storage - .find_item_exist_labels(payload.item_id) - .await - .unwrap(); - - let old_ids: HashSet = old_labels.iter().map(|l| l.label_id).collect(); - let new_ids: HashSet = label_ids.iter().copied().collect(); - - let to_add: Vec = new_ids.difference(&old_ids).copied().collect(); - let to_remove: Vec = old_ids.difference(&new_ids).copied().collect(); - - issue_storage - .modify_labels(&user.username, item_id, &link, to_add, to_remove) - .await?; - Ok(Json(CommonResult::success(None))) + api_common::label_assignee::assignees_update(user, state, payload, String::from("issue")).await } diff --git a/mono/src/api/issue/mod.rs b/mono/src/api/issue/mod.rs index 89ab18208..d9f9e2cb7 100644 --- a/mono/src/api/issue/mod.rs +++ b/mono/src/api/issue/mod.rs @@ -1,4 +1,5 @@ -use callisto::{label, mega_issue}; +use callisto::mega_issue; +use jupiter::storage::stg_common::model::{ItemDetails, ItemKind}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -7,28 +8,49 @@ use crate::api::{label::LabelItem, mr::MegaConversation}; pub mod issue_router; #[derive(Serialize, Deserialize, ToSchema)] -pub struct IssueItem { +pub struct ItemRes { pub link: String, pub title: String, pub status: String, - pub user_id: String, - pub labels: Vec, + pub author: String, pub open_timestamp: i64, pub closed_at: Option, + pub merge_timestamp: Option, pub updated_at: i64, + pub labels: Vec, + pub assignees: Vec, + pub comment_num: usize, } -impl From<(mega_issue::Model, Vec)> for IssueItem { - fn from(value: (mega_issue::Model, Vec)) -> Self { - Self { - link: value.0.link, - title: value.0.title, - status: value.0.status.to_string(), - user_id: value.0.user_id, - open_timestamp: value.0.created_at.and_utc().timestamp(), - closed_at: value.0.closed_at.map(|dt| dt.and_utc().timestamp()), - updated_at: value.0.updated_at.and_utc().timestamp(), - labels: value.1.into_iter().map(|m| m.into()).collect() +impl From for ItemRes { + fn from(value: ItemDetails) -> Self { + match value.item { + ItemKind::Issue(model) => Self { + link: model.link, + title: model.title, + status: model.status.to_string(), + author: model.author, + open_timestamp: model.created_at.and_utc().timestamp(), + merge_timestamp: None, + closed_at: model.closed_at.map(|dt| dt.and_utc().timestamp()), + updated_at: model.updated_at.and_utc().timestamp(), + labels: value.labels.into_iter().map(|m| m.into()).collect(), + assignees: value.assignees, + comment_num: value.comment_num, + }, + ItemKind::Mr(model) => Self { + link: model.link, + title: model.title, + status: format!("{:?}", model.status), + author: String::new(), + open_timestamp: model.created_at.and_utc().timestamp(), + merge_timestamp: model.merge_date.map(|dt| dt.and_utc().timestamp()), + closed_at: None, + updated_at: model.updated_at.and_utc().timestamp(), + labels: value.labels.into_iter().map(|m| m.into()).collect(), + assignees: value.assignees, + comment_num: value.comment_num, + }, } } } @@ -61,10 +83,3 @@ impl From for IssueDetail { } } } - -#[derive(Deserialize, ToSchema)] -pub struct LabelUpdatePayload { - label_ids: Vec, - item_id: i64, - link: String, -} diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index 40a17d115..4aec1951c 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -23,6 +23,7 @@ use jupiter::{ use crate::api::oauth::campsite_store::CampsiteApiStore; +pub mod api_common; pub mod api_router; pub mod error; pub mod issue; diff --git a/mono/src/api/mr/mod.rs b/mono/src/api/mr/mod.rs index cb9a60469..41155218c 100644 --- a/mono/src/api/mr/mod.rs +++ b/mono/src/api/mr/mod.rs @@ -1,46 +1,14 @@ use serde::{Deserialize, Serialize}; use callisto::{ - label, mega_conversation, mega_mr, + mega_conversation, mega_mr, sea_orm_active_enums::{ConvTypeEnum, MergeStatusEnum}, }; use utoipa::ToSchema; use uuid::Uuid; -use crate::api::label::LabelItem; - pub mod mr_router; -#[derive(Deserialize, ToSchema)] -pub struct MRStatusParams { - pub status: String, -} - -#[derive(Serialize, Deserialize, ToSchema)] -pub struct MrInfoItem { - pub link: String, - pub title: String, - pub status: MergeStatusEnum, - pub open_timestamp: i64, - pub merge_timestamp: Option, - pub updated_at: i64, - pub labels: Vec, -} - -impl From<(mega_mr::Model, Vec)> for MrInfoItem { - fn from(value: (mega_mr::Model, Vec)) -> Self { - Self { - link: value.0.link, - title: value.0.title, - status: value.0.status, - open_timestamp: value.0.created_at.and_utc().timestamp(), - merge_timestamp: value.0.merge_date.map(|dt| dt.and_utc().timestamp()), - updated_at: value.0.updated_at.and_utc().timestamp(), - labels: value.1.into_iter().map(|m| m.into()).collect(), - } - } -} - #[derive(Serialize, Deserialize, ToSchema)] pub struct MRDetail { pub id: i64, diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index 968f368c3..96ed84324 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -14,11 +14,10 @@ use common::{ use saturn::ActionEnum; use crate::api::{ - issue::{issue_router::common_label_update, LabelUpdatePayload}, - mr::{ - FilesChangedList, MRDetail, MRStatusParams, MrInfoItem, MuiTreeNode, - SaveCommentRequest, - }, + api_common, + api_common::model::{AssigneeUpdatePayload, LabelUpdatePayload, ListPayload}, + issue::ItemRes, + mr::{FilesChangedList, MRDetail, MuiTreeNode, SaveCommentRequest}, oauth::model::LoginUser, }; use crate::api::{util, MonoApiServiceState}; @@ -36,7 +35,8 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(mr_files_changed)) .routes(routes!(save_comment)) .routes(routes!(delete_comment)) - .routes(routes!(labels)), + .routes(routes!(labels)) + .routes(routes!(assignees)), ) } @@ -160,10 +160,7 @@ async fn merge( ) .await .unwrap(); - state - .monorepo() - .merge_mr(&user.username, model) - .await?; + state.monorepo().merge_mr(&user.username, model).await?; } Ok(Json(CommonResult::success(None))) } @@ -172,31 +169,19 @@ async fn merge( #[utoipa::path( post, path = "/list", - request_body = PageParams, + request_body = PageParams, responses( - (status = 200, body = CommonResult>, content_type = "application/json") + (status = 200, body = CommonResult>, content_type = "application/json") ), tag = MR_TAG )] async fn fetch_mr_list( state: State, - Json(json): Json>, -) -> Result>>, ApiError> { - let status = json.additional.status; - let status = if status == "open" { - vec![MergeStatusEnum::Open] - } else if status == "closed" { - vec![MergeStatusEnum::Closed, MergeStatusEnum::Merged] - } else { - vec![ - MergeStatusEnum::Open, - MergeStatusEnum::Closed, - MergeStatusEnum::Merged, - ] - }; + Json(json): Json>, +) -> Result>>, ApiError> { let (items, total) = state .mr_stg() - .get_mr_by_status(status, json.pagination) + .get_mr_list(json.additional.into(), json.pagination) .await?; let res = CommonPage { items: items.into_iter().map(|m| m.into()).collect(), @@ -350,7 +335,25 @@ async fn labels( state: State, Json(payload): Json, ) -> Result>, ApiError> { - common_label_update(user, state, payload).await + api_common::label_assignee::label_update(user, state, payload, String::from("mr")).await +} + +/// update MR related assignees +#[utoipa::path( + post, + path = "/assignees", + request_body = AssigneeUpdatePayload, + responses( + (status = 200, body = CommonResult, content_type = "application/json") + ), + tag = MR_TAG +)] +async fn assignees( + user: LoginUser, + state: State, + Json(payload): Json, +) -> Result>, ApiError> { + api_common::label_assignee::assignees_update(user, state, payload, String::from("mr")).await } fn build_forest(paths: Vec) -> Vec { diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index dd036d03c..fa20bc7eb 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -3033,20 +3033,32 @@ export type FigmaKeyPair = { write_key: string } -export type CommonResultCommonPageIssueItem = { +export type AssigneeUpdatePayload = { + assignees: string[] + /** @format int64 */ + item_id: number + link: string +} + +export type CommonResultCommonPageItemRes = { data?: { items: { + assignees: string[] + author: string /** @format int64 */ closed_at?: number | null + /** @min 0 */ + comment_num: number labels: LabelItem[] link: string /** @format int64 */ + merge_timestamp?: number | null + /** @format int64 */ open_timestamp: number status: string title: string /** @format int64 */ updated_at: number - user_id: string }[] /** * @format int64 @@ -3077,30 +3089,6 @@ export type CommonResultCommonPageLabelItem = { req_result: boolean } -export type CommonResultCommonPageMrInfoItem = { - data?: { - items: { - labels: LabelItem[] - link: string - /** @format int64 */ - merge_timestamp?: number | null - /** @format int64 */ - open_timestamp: number - status: MergeStatusEnum - title: string - /** @format int64 */ - updated_at: number - }[] - /** - * @format int64 - * @min 0 - */ - total: number - } - err_message: string - req_result: boolean -} - export type CommonResultFilesChangedList = { data?: { content: string @@ -3182,7 +3170,8 @@ export enum ConvTypeEnum { Merged = 'Merged', Closed = 'Closed', Reopen = 'Reopen', - Label = 'Label' + Label = 'Label', + Assignee = 'Assignee' } export type CreateFileInfo = { @@ -3205,18 +3194,23 @@ export type FilesChangedList = { mui_trees: MuiTreeNode[] } -export type IssueItem = { +export type ItemRes = { + assignees: string[] + author: string /** @format int64 */ closed_at?: number | null + /** @min 0 */ + comment_num: number labels: LabelItem[] link: string /** @format int64 */ + merge_timestamp?: number | null + /** @format int64 */ open_timestamp: number status: string title: string /** @format int64 */ updated_at: number - user_id: string } export type LabelItem = { @@ -3243,6 +3237,15 @@ export type LatestCommitInfo = { status: string } +export type ListPayload = { + asc: boolean + assignees?: any[] | null + author?: string | null + labels?: any[] | null + sort_by?: string | null + status: string +} + export type MRDetail = { conversations: MegaConversation[] /** @format int64 */ @@ -3256,10 +3259,6 @@ export type MRDetail = { title: string } -export type MRStatusParams = { - status: string -} - export type MegaConversation = { comment?: string | null conv_type: ConvTypeEnum @@ -3278,19 +3277,6 @@ export enum MergeStatusEnum { Closed = 'Closed' } -export type MrInfoItem = { - labels: LabelItem[] - link: string - /** @format int64 */ - merge_timestamp?: number | null - /** @format int64 */ - open_timestamp: number - status: MergeStatusEnum - title: string - /** @format int64 */ - updated_at: number -} - export type MuiTreeNode = { children?: any[] | null id: string @@ -3308,15 +3294,13 @@ export type NewLabel = { name: string } -export type PageParamsMRStatusParams = { - additional: { - status: string - } - pagination: Pagination -} - -export type PageParamsStatusParams = { +export type PageParamsListPayload = { additional: { + asc: boolean + assignees?: any[] | null + author?: string | null + labels?: any[] | null + sort_by?: string | null status: string } pagination: Pagination @@ -3344,10 +3328,6 @@ export type SaveCommentRequest = { content: string } -export type StatusParams = { - status: string -} - export type TreeBriefItem = { content_type: string name: string @@ -4519,11 +4499,13 @@ export type GetApiBlobData = CommonResultString export type PostApiCreateFileData = CommonResultString +export type PostApiIssueAssigneesData = CommonResultString + export type DeleteApiIssueCommentDeleteData = CommonResultString export type PostApiIssueLabelsData = CommonResultString -export type PostApiIssueListData = CommonResultCommonPageIssueItem +export type PostApiIssueListData = CommonResultCommonPageItemRes export type PostApiIssueNewData = CommonResultString @@ -4546,11 +4528,13 @@ export type GetApiLatestCommitParams = { export type GetApiLatestCommitData = LatestCommitInfo +export type PostApiMrAssigneesData = CommonResultString + export type DeleteApiMrCommentDeleteData = CommonResultString export type PostApiMrLabelsData = CommonResultString -export type PostApiMrListData = CommonResultCommonPageMrInfoItem +export type PostApiMrListData = CommonResultCommonPageItemRes export type PostApiMrCloseData = CommonResultString @@ -12928,6 +12912,31 @@ export class Api extends HttpClient { + const base = 'POST:/api/v1/issue/assignees' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: AssigneeUpdatePayload, params: RequestParams = {}) => + this.request({ + path: `/api/v1/issue/assignees`, + method: 'POST', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + /** * No description * @@ -12990,7 +12999,7 @@ export class Api extends HttpClient([base]), requestKey: () => dataTaggedQueryKey([base]), - request: (data: PageParamsStatusParams, params: RequestParams = {}) => + request: (data: PageParamsListPayload, params: RequestParams = {}) => this.request({ path: `/api/v1/issue/list`, method: 'POST', @@ -13194,6 +13203,31 @@ export class Api extends HttpClient { + const base = 'POST:/api/v1/mr/assignees' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: AssigneeUpdatePayload, params: RequestParams = {}) => + this.request({ + path: `/api/v1/mr/assignees`, + method: 'POST', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + /** * No description * @@ -13256,7 +13290,7 @@ export class Api extends HttpClient([base]), requestKey: () => dataTaggedQueryKey([base]), - request: (data: PageParamsMRStatusParams, params: RequestParams = {}) => + request: (data: PageParamsListPayload, params: RequestParams = {}) => this.request({ path: `/api/v1/mr/list`, method: 'POST', diff --git a/vault/src/integration/vault_core.rs b/vault/src/integration/vault_core.rs index ca24c9d82..199aa05ab 100644 --- a/vault/src/integration/vault_core.rs +++ b/vault/src/integration/vault_core.rs @@ -49,10 +49,9 @@ impl VaultCore { pub fn new(ctx: Storage) -> Self { let dir = common::config::mega_base().join("vault"); let key_path = dir.join(CORE_KEY_FILE); - println!("{:?}", key_path); + tracing::info!("{key_path:?}"); std::fs::create_dir_all(&dir).expect("Failed to create vault directory"); - let result = Self::config(ctx.clone(), key_path); - result + Self::config(ctx.clone(), key_path) } fn config(ctx: Storage, key_path: PathBuf) -> Self {