From 807286532053e41c2066fa000a892a869025d681 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Fri, 20 Jun 2025 17:13:57 +0800 Subject: [PATCH] feat(UI): add label apis --- ceres/src/api_service/mono_api_service.rs | 13 +- ceres/src/pack/monorepo.rs | 84 +++--- ceres/src/protocol/mod.rs | 1 - ceres/src/protocol/mr.rs | 74 ----- common/src/errors.rs | 2 +- common/src/model.rs | 8 + config/config.toml | 6 +- jupiter/callisto/src/item_labels.rs | 21 ++ jupiter/callisto/src/label.rs | 21 ++ jupiter/callisto/src/mega_issue.rs | 2 +- jupiter/callisto/src/mod.rs | 2 + jupiter/callisto/src/prelude.rs | 2 + jupiter/callisto/src/sea_orm_active_enums.rs | 2 + jupiter/migration/README.md | 2 +- jupiter/migration/src/lib.rs | 2 + .../src/m20250618_065050_add_label.rs | 101 +++++++ jupiter/src/storage/issue_storage.rs | 155 +++++++++-- jupiter/src/storage/mr_storage.rs | 124 ++++----- mono/src/api/api_router.rs | 8 +- mono/src/api/error.rs | 6 +- mono/src/api/issue/issue_router.rs | 156 +++++++---- mono/src/api/issue/mod.rs | 15 +- mono/src/api/label/label_router.rs | 65 +++++ mono/src/api/label/mod.rs | 31 +++ mono/src/api/mod.rs | 1 + mono/src/api/mr/mr_router.rs | 263 +++++++++--------- mono/src/api/oauth/campsite_store.rs | 3 +- mono/src/api/oauth/mod.rs | 5 +- mono/src/api/user/user_router.rs | 66 ++--- mono/src/server/https_server.rs | 4 +- moon/apps/web/components/Issues/IssuePage.tsx | 2 +- .../web/components/Issues/IssuesContent.tsx | 4 +- moon/packages/types/generated.ts | 164 ++++++++++- third-party/Cargo.toml | 4 +- 34 files changed, 927 insertions(+), 492 deletions(-) delete mode 100644 ceres/src/protocol/mr.rs create mode 100644 jupiter/callisto/src/item_labels.rs create mode 100644 jupiter/callisto/src/label.rs create mode 100644 jupiter/migration/src/m20250618_065050_add_label.rs create mode 100644 mono/src/api/label/label_router.rs create mode 100644 mono/src/api/label/mod.rs diff --git a/ceres/src/api_service/mono_api_service.rs b/ceres/src/api_service/mono_api_service.rs index 424f58571..73481a285 100644 --- a/ceres/src/api_service/mono_api_service.rs +++ b/ceres/src/api_service/mono_api_service.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use tokio::process::Command; use callisto::sea_orm_active_enums::ConvTypeEnum; -use callisto::{mega_blob, mega_tree, raw_blob}; +use callisto::{mega_blob, mega_mr, mega_tree, raw_blob}; use common::errors::MegaError; use jupiter::context::Context; use jupiter::storage::batch_save_model; @@ -20,7 +20,6 @@ use mercury::internal::object::tree::{Tree, TreeItem, TreeItemMode}; use crate::api_service::ApiHandler; use crate::model::git::CreateFileInfo; -use crate::protocol::mr::MergeRequest; #[derive(Clone)] pub struct MonoApiService { @@ -233,7 +232,7 @@ impl ApiHandler for MonoApiService { } impl MonoApiService { - pub async fn merge_mr(&self, user_id: String, mr: &mut MergeRequest) -> Result<(), MegaError> { + pub async fn merge_mr(&self, user_id: String, mr: mega_mr::Model) -> Result<(), MegaError> { let storage = self.context.services.mono_storage.clone(); let refs = storage.get_ref(&mr.path).await.unwrap().unwrap(); @@ -259,18 +258,16 @@ impl MonoApiService { storage.remove_none_mr_refs(&mr.path).await.unwrap(); // TODO: self.clean_dangling_commits().await; } - // update mr - mr.merge(); // add conversation self.context - .mr_stg() - .add_mr_conversation(&mr.link, user_id, ConvTypeEnum::Merged, None) + .issue_stg() + .add_conversation(&mr.link, &user_id, None, ConvTypeEnum::Merged) .await .unwrap(); // update mr status last self.context .mr_stg() - .update_mr(mr.clone().into()) + .merge_mr(mr) .await .unwrap(); } else { diff --git a/ceres/src/pack/monorepo.rs b/ceres/src/pack/monorepo.rs index b342c4e3d..7260968c7 100644 --- a/ceres/src/pack/monorepo.rs +++ b/ceres/src/pack/monorepo.rs @@ -11,12 +11,12 @@ use futures::future::join_all; use tokio::sync::mpsc::{self, UnboundedReceiver}; use tokio_stream::wrappers::ReceiverStream; -use callisto::{raw_blob, sea_orm_active_enums::ConvTypeEnum}; +use callisto::{mega_mr, raw_blob, sea_orm_active_enums::ConvTypeEnum}; use common::{ errors::MegaError, utils::{self, MEGA_BRANCH_NAME}, }; -use jupiter::{context::Context, storage::mr_storage::MrStorage}; +use jupiter::context::Context; use mercury::internal::{object::ObjectTrait, pack::encode::PackEncoder}; use mercury::{ errors::GitError, @@ -29,10 +29,7 @@ use mercury::{ use crate::{ pack::PackHandler, - protocol::{ - import_refs::{RefCommand, Refs}, - mr::MergeRequest, - }, + protocol::import_refs::{RefCommand, Refs}, }; pub struct MonoRepo { @@ -290,7 +287,7 @@ impl PackHandler for MonoRepo { let storage = self.context.services.mono_storage.clone(); if let Some(c) = commit { - let mr_link = self.handle_mr(&c.format_message()).await?; + let mr_link = self.handle_mr(&c.format_message()).await.unwrap(); let ref_name = utils::mr_ref_name(&mr_link); if let Some(mut mr_ref) = storage.get_mr_ref(&ref_name).await.unwrap() { mr_ref.ref_commit_hash = refs.new_id.clone(); @@ -328,79 +325,64 @@ impl PackHandler for MonoRepo { } impl MonoRepo { - async fn handle_mr(&self, title: &str) -> Result { + async fn handle_mr(&self, title: &str) -> Result { let storage = self.context.mr_stg(); let path_str = self.path.to_str().unwrap(); match storage.get_open_mr_by_path(path_str).await.unwrap() { Some(mr) => { - let mut mr = mr.into(); - self.handle_existing_mr(&mut mr, &storage).await + let link = mr.link.clone(); + self.handle_existing_mr(mr).await?; + Ok(link) } None => { if self.from_hash == "0".repeat(40) { - return Err(GitError::CustomError(String::from( + return Err(MegaError::with_message( "Can not init directory under monorepo directory!", - ))); + )); } - let link: String = utils::generate_link(); - let mr = MergeRequest { - path: path_str.to_owned(), - from_hash: self.from_hash.clone(), - to_hash: self.to_hash.clone(), - link: link.clone(), - title: title.to_string(), - ..Default::default() - }; - storage.save_mr(mr.clone().into()).await.unwrap(); + let link = storage + .new_mr(path_str, title, &self.from_hash, &self.to_hash) + .await + .unwrap(); Ok(link) } } } - async fn handle_existing_mr( - &self, - mr: &mut MergeRequest, - storage: &MrStorage, - ) -> Result { + async fn handle_existing_mr(&self, mr: mega_mr::Model) -> Result<(), MegaError> { + let mr_stg = self.context.mr_stg(); + let issue_stg = self.context.issue_stg(); if mr.from_hash == self.from_hash { if mr.to_hash != self.to_hash { - let comment = self.comment_for_force_update(&mr.to_hash, &self.to_hash); - mr.to_hash = self.to_hash.clone(); - storage - .add_mr_conversation( + issue_stg + .add_conversation( &mr.link, - String::new(), + "", + Some(format!( + "Mega updated the mr automatic from {} to {}", + &mr.to_hash[..6], + &self.to_hash[..6] + )), ConvTypeEnum::ForcePush, - Some(comment), ) - .await - .unwrap(); + .await?; + mr_stg.update_mr_to_hash(mr, &self.to_hash).await?; } else { tracing::info!("repeat commit with mr: {}, do nothing", mr.id); } } else { - mr.close(); - storage - .add_mr_conversation( + issue_stg + .add_conversation( &mr.link, - String::new(), - ConvTypeEnum::Closed, + "", Some("Mega closed MR due to conflict".to_string()), + ConvTypeEnum::Closed, ) .await .unwrap(); + mr_stg.close_mr(mr).await?; } - - storage.update_mr(mr.clone().into()).await.unwrap(); - Ok(mr.link.clone()) - } - - fn comment_for_force_update(&self, from: &str, to: &str) -> String { - format!( - "Mega updated the mr automatic from {} to {}", - &from[..6], - &to[..6] - ) + Ok(()) } } diff --git a/ceres/src/protocol/mod.rs b/ceres/src/protocol/mod.rs index dce72e74a..2a30d2812 100644 --- a/ceres/src/protocol/mod.rs +++ b/ceres/src/protocol/mod.rs @@ -13,7 +13,6 @@ use repo::Repo; use crate::pack::{import_repo::ImportRepo, monorepo::MonoRepo, PackHandler}; pub mod import_refs; -pub mod mr; pub mod repo; pub mod smart; diff --git a/ceres/src/protocol/mr.rs b/ceres/src/protocol/mr.rs deleted file mode 100644 index d345a7406..000000000 --- a/ceres/src/protocol/mr.rs +++ /dev/null @@ -1,74 +0,0 @@ -use chrono::NaiveDateTime; - -use callisto::{mega_mr, sea_orm_active_enums::MergeStatusEnum}; -use common::utils::generate_id; - -#[derive(Clone)] -pub struct MergeRequest { - pub id: i64, - pub link: String, - pub title: String, - pub status: MergeStatusEnum, - pub merge_date: Option, - pub path: String, - pub from_hash: String, - pub to_hash: String, -} - -impl Default for MergeRequest { - fn default() -> Self { - Self { - id: generate_id(), - link: String::new(), - title: String::new(), - status: MergeStatusEnum::Open, - merge_date: None, - path: String::new(), - from_hash: String::new(), - to_hash: String::new(), - } - } -} - -impl MergeRequest { - pub fn close(&mut self) { - self.status = MergeStatusEnum::Closed; - } - - pub fn merge(&mut self) { - self.status = MergeStatusEnum::Merged; - self.merge_date = Some(chrono::Utc::now().naive_utc()) - } -} - -impl From for mega_mr::Model { - fn from(value: MergeRequest) -> Self { - Self { - id: value.id, - link: value.link, - title: value.title, - status: value.status, - merge_date: value.merge_date, - path: value.path, - from_hash: value.from_hash, - to_hash: value.to_hash, - created_at: chrono::Utc::now().naive_utc(), - updated_at: chrono::Utc::now().naive_utc(), - } - } -} - -impl From for MergeRequest { - fn from(value: mega_mr::Model) -> Self { - Self { - id: value.id, - link: value.link, - title: value.title, - status: value.status, - merge_date: value.merge_date, - path: value.path, - from_hash: value.from_hash, - to_hash: value.to_hash, - } - } -} diff --git a/common/src/errors.rs b/common/src/errors.rs index 093da1c1e..2c398667c 100644 --- a/common/src/errors.rs +++ b/common/src/errors.rs @@ -10,7 +10,7 @@ use crate::model::CommonResult; pub type MegaResult = Result<(), MegaError>; -#[derive(Debug)] +#[derive(Error, Debug)] pub struct MegaError { pub error: Option, pub code: i32, diff --git a/common/src/model.rs b/common/src/model.rs index f47c2786a..c1eb6096f 100644 --- a/common/src/model.rs +++ b/common/src/model.rs @@ -45,6 +45,14 @@ impl CommonResult { err_message: err_message.to_string(), } } + + pub fn common_failed() -> Self { + CommonResult { + req_result: false, + data: None, + err_message: String::from("API request error"), + } + } } #[derive(Deserialize, ToSchema)] diff --git a/config/config.toml b/config/config.toml index 11e54db77..10030a1fd 100644 --- a/config/config.toml +++ b/config/config.toml @@ -115,8 +115,8 @@ ui_domain = "http://local.gitmega.com" # Set your own domain here, for example: .gitmono.com cookie_domain = "localhost" -# Used for call api from campsite server -campsite_api_domain = "http://api.gitmega.com" +# Used for call api from campsite server, for example: http://api.gitmono.test:3001 +campsite_api_domain = "http://api.gitmono.test:3001" # allowed cors origins -allowed_cors_origins = ["http://local.gitmega.com", "http://app.gitmega.com"] \ No newline at end of file +allowed_cors_origins = ["http://local.gitmega.com", "http://app.gitmega.com", "http://app.gitmono.test"] \ No newline at end of file diff --git a/jupiter/callisto/src/item_labels.rs b/jupiter/callisto/src/item_labels.rs new file mode 100644 index 000000000..ff764aca3 --- /dev/null +++ b/jupiter/callisto/src/item_labels.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "item_labels")] +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 label_id: i64, + pub item_type: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/label.rs b/jupiter/callisto/src/label.rs new file mode 100644 index 000000000..7d7ffdf59 --- /dev/null +++ b/jupiter/callisto/src/label.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "label")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key, auto_increment = false)] + pub id: i64, + pub name: String, + pub color: String, + pub description: 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 243d71f6c..6a18d537f 100644 --- a/jupiter/callisto/src/mega_issue.rs +++ b/jupiter/callisto/src/mega_issue.rs @@ -11,11 +11,11 @@ pub struct Model { #[sea_orm(unique)] pub link: String, pub title: String, - pub owner: i64, pub status: String, pub created_at: DateTime, pub updated_at: DateTime, pub closed_at: Option, + pub user_id: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/jupiter/callisto/src/mod.rs b/jupiter/callisto/src/mod.rs index 1ceda5a4c..08bea32de 100644 --- a/jupiter/callisto/src/mod.rs +++ b/jupiter/callisto/src/mod.rs @@ -12,6 +12,8 @@ pub mod git_repo; pub mod git_tag; pub mod git_tree; pub mod import_refs; +pub mod item_labels; +pub mod label; pub mod lfs_locks; pub mod lfs_objects; pub mod lfs_split_relations; diff --git a/jupiter/callisto/src/prelude.rs b/jupiter/callisto/src/prelude.rs index d9cab32f3..30d5559be 100644 --- a/jupiter/callisto/src/prelude.rs +++ b/jupiter/callisto/src/prelude.rs @@ -10,6 +10,8 @@ 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_labels::Entity as ItemLabels; +pub use super::label::Entity as Label; pub use super::lfs_locks::Entity as LfsLocks; pub use super::lfs_objects::Entity as LfsObjects; pub use super::lfs_split_relations::Entity as LfsSplitRelations; diff --git a/jupiter/callisto/src/sea_orm_active_enums.rs b/jupiter/callisto/src/sea_orm_active_enums.rs index b8d3ec6c6..6096fc0cb 100644 --- a/jupiter/callisto/src/sea_orm_active_enums.rs +++ b/jupiter/callisto/src/sea_orm_active_enums.rs @@ -31,6 +31,8 @@ pub enum ConvTypeEnum { Closed, #[sea_orm(string_value = "reopen")] Reopen, + #[sea_orm(string_value = "label")] + Label, } #[derive( Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema, diff --git a/jupiter/migration/README.md b/jupiter/migration/README.md index 6a363b772..b1f67f008 100644 --- a/jupiter/migration/README.md +++ b/jupiter/migration/README.md @@ -5,7 +5,7 @@ sea-orm-cli migrate generate cd mega/jupiter/migration -sea-orm-cli generate entity -u postgres://username:password@hostname/mono -o ../callisto/src --with-serde both +sea-orm-cli generate entity -u postgres://postgres:postgres@localhost:5432/mono -o ../callisto/src --with-serde both ``` diff --git a/jupiter/migration/src/lib.rs b/jupiter/migration/src/lib.rs index 7cdc11f6a..e6630ef4a 100644 --- a/jupiter/migration/src/lib.rs +++ b/jupiter/migration/src/lib.rs @@ -5,6 +5,7 @@ mod m20250314_025943_init; mod m20250427_031332_add_mr_refs_tag; mod m20250605_013340_alter_mega_mr_index; mod m20250613_033821_alter_user_id; +mod m20250618_065050_add_label; pub struct Migrator; @@ -16,6 +17,7 @@ impl MigratorTrait for Migrator { Box::new(m20250427_031332_add_mr_refs_tag::Migration), Box::new(m20250605_013340_alter_mega_mr_index::Migration), Box::new(m20250613_033821_alter_user_id::Migration), + Box::new(m20250618_065050_add_label::Migration), ] } } diff --git a/jupiter/migration/src/m20250618_065050_add_label.rs b/jupiter/migration/src/m20250618_065050_add_label.rs new file mode 100644 index 000000000..a83a92d1d --- /dev/null +++ b/jupiter/migration/src/m20250618_065050_add_label.rs @@ -0,0 +1,101 @@ +use sea_orm_migration::{prelude::*, schema::*, sea_orm::DatabaseBackend}; + +use crate::pk_bigint; + +#[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 'label';"#, + ) + .await?; + } + DatabaseBackend::Sqlite | DatabaseBackend::MySql => {} + } + + manager + .create_table( + table_auto(Label::Table) + .col(pk_bigint(Label::Id)) + .col(string(Label::Name)) + .col(string(Label::Color)) + .col(string(Label::Description)) + .to_owned(), + ) + .await?; + + manager + .create_table( + table_auto(ItemLabels::Table) + .col(big_integer(ItemLabels::ItemId)) + .col(big_integer(ItemLabels::LabelId)) + .col(string(ItemLabels::ItemType)) + .primary_key( + Index::create() + .col(ItemLabels::ItemId) + .col(ItemLabels::LabelId), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(MegaIssue::Table) + .drop_column("owner") + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(MegaIssue::Table) + .add_column_if_not_exists( + ColumnDef::new(Alias::new("user_id")) + .string() + .not_null() + .default(""), + ) + .to_owned(), + ) + .await?; + Ok(()) + } + + async fn down(&self, _: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Label { + Table, + Id, + Name, + Color, + Description, +} + +#[derive(DeriveIden)] +enum ItemLabels { + Table, + ItemId, + LabelId, + ItemType, +} + +#[derive(DeriveIden)] +enum MegaIssue { + Table, +} diff --git a/jupiter/src/storage/issue_storage.rs b/jupiter/src/storage/issue_storage.rs index 69acf760f..bcdca6047 100644 --- a/jupiter/src/storage/issue_storage.rs +++ b/jupiter/src/storage/issue_storage.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; - use sea_orm::{ ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, - PaginatorTrait, QueryFilter, QueryOrder, Set, + PaginatorTrait, QueryFilter, QueryOrder, Set, TransactionTrait, }; +use std::sync::Arc; use callisto::sea_orm_active_enums::ConvTypeEnum; -use callisto::{mega_conversation, mega_issue}; +use callisto::{item_labels, label, mega_conversation, mega_issue}; use common::errors::MegaError; use common::model::Pagination; use common::utils::{generate_id, generate_link}; @@ -41,10 +40,19 @@ impl IssueStorage { .order_by_desc(mega_issue::Column::CreatedAt) .paginate(self.get_connection(), page.per_page); let num_pages = paginator.num_items().await?; - Ok(paginator + let (issues, page) = paginator .fetch_page(page.page - 1) .await - .map(|m| (m, num_pages))?) + .map(|m| (m, num_pages))?; + + // let issue_ids: Vec = issues.iter().map(|i| i.id).collect(); + // let item_label_with_labels = item_labels::Entity::find() + // .filter(item_labels::Column::ItemId.is_in(issue_ids.clone())) + // .find_also_related(label::Entity) + // .all(self.get_connection()) + // .await?; + + Ok((issues, page)) } pub async fn get_issue(&self, link: &str) -> Result, MegaError> { @@ -56,28 +64,34 @@ impl IssueStorage { Ok(model) } + pub async fn get_issue_by_id(&self, id: i64) -> Result, MegaError> { + let model = mega_issue::Entity::find_by_id(id) + .one(self.get_connection()) + .await + .unwrap(); + Ok(model) + } + pub async fn save_issue( &self, - user_id: i64, + user_id: &str, title: &str, ) -> Result { let model = mega_issue::Model { id: generate_id(), link: generate_link(), title: title.to_owned(), - owner: user_id, + user_id: user_id.to_owned(), status: "open".to_owned(), created_at: chrono::Utc::now().naive_utc(), updated_at: chrono::Utc::now().naive_utc(), closed_at: None, }; - model - .clone() + let res = model .into_active_model() .insert(self.get_connection()) - .await - .unwrap(); - Ok(model) + .await?; + Ok(res) } pub async fn close_issue(&self, link: &str) -> Result<(), MegaError> { @@ -98,7 +112,7 @@ impl IssueStorage { Ok(()) } - pub async fn get_issue_conversations( + pub async fn get_conversations( &self, link: &str, ) -> Result, MegaError> { @@ -109,7 +123,7 @@ impl IssueStorage { Ok(model?) } - pub async fn remove_issue_conversation(&self, id: i64) -> Result<(), MegaError> { + pub async fn remove_conversation(&self, id: i64) -> Result<(), MegaError> { mega_conversation::Entity::delete_by_id(id) .exec(self.get_connection()) .await @@ -117,17 +131,18 @@ impl IssueStorage { Ok(()) } - pub async fn add_issue_conversation( + pub async fn add_conversation( &self, link: &str, - user_id: String, + user_id: &str, comment: Option, + conv_type: ConvTypeEnum, ) -> Result { let conversation = mega_conversation::Model { id: generate_id(), link: link.to_owned(), - user_id, - conv_type: ConvTypeEnum::Comment, + user_id: user_id.to_owned(), + conv_type, comment, created_at: chrono::Utc::now().naive_utc(), updated_at: chrono::Utc::now().naive_utc(), @@ -136,4 +151,106 @@ impl IssueStorage { let res = conversation.insert(self.get_connection()).await.unwrap(); Ok(res.id) } + + pub async fn new_label( + &self, + name: &str, + color: &str, + description: &str, + ) -> Result { + let model = label::Model { + id: generate_id(), + name: name.to_owned(), + color: color.to_owned(), + description: description.to_owned(), + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + }; + let res = model + .into_active_model() + .insert(self.get_connection()) + .await?; + Ok(res) + } + + pub async fn list_labels_by_page( + &self, + page: Pagination, + ) -> Result<(Vec, u64), MegaError> { + let paginator = label::Entity::find().paginate(self.get_connection(), page.per_page); + let num_pages = paginator.num_items().await?; + Ok(paginator + .fetch_page(page.page - 1) + .await + .map(|m| (m, num_pages))?) + } + + pub async fn find_item_exist_labels( + &self, + item_id: i64, + ) -> Result, MegaError> { + let item_labels = item_labels::Entity::find() + .filter(item_labels::Column::ItemId.eq(item_id)) + .all(self.get_connection()) + .await?; + Ok(item_labels) + } + + pub async fn modify_labels( + &self, + user_id: &str, + item_id: i64, + link: &str, + to_add: Vec, + to_remove: Vec, + ) -> Result<(), MegaError> { + let txn = self.get_connection().begin().await?; + + if !to_remove.is_empty() { + item_labels::Entity::delete_many() + .filter(item_labels::Column::ItemId.eq(item_id)) + .filter(item_labels::Column::LabelId.is_in(to_remove.clone())) + .exec(&txn) + .await?; + + self.add_conversation( + link, + user_id, + Some(format!("{} removed {:?}", user_id, to_remove)), + ConvTypeEnum::Label, + ) + .await?; + } + + if !to_add.is_empty() { + let mut new_item_labels = Vec::new(); + for label_id in to_add.clone() { + new_item_labels.push( + item_labels::Model { + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + item_id, + label_id, + item_type: String::from("issue"), + } + .into_active_model(), + ); + } + + item_labels::Entity::insert_many(new_item_labels) + .exec(&txn) + .await?; + self.add_conversation( + link, + user_id, + Some(format!("{} added {:?}", user_id, to_add)), + ConvTypeEnum::Label, + ) + .await?; + } + + txn.commit().await?; + + Ok(()) + } } diff --git a/jupiter/src/storage/mr_storage.rs b/jupiter/src/storage/mr_storage.rs index 39cdb9d10..b8b3f5304 100644 --- a/jupiter/src/storage/mr_storage.rs +++ b/jupiter/src/storage/mr_storage.rs @@ -5,8 +5,8 @@ use sea_orm::{ PaginatorTrait, QueryFilter, QueryOrder, Set, }; -use callisto::sea_orm_active_enums::{ConvTypeEnum, MergeStatusEnum}; -use callisto::{mega_conversation, mega_mr}; +use callisto::mega_mr; +use callisto::sea_orm_active_enums::MergeStatusEnum; use common::errors::MegaError; use common::utils::generate_id; @@ -64,98 +64,70 @@ impl MrStorage { let model = mega_mr::Entity::find() .filter(mega_mr::Column::Link.eq(link)) .one(self.get_connection()) - .await - .unwrap(); + .await?; Ok(model) } - pub async fn save_mr(&self, mr: mega_mr::Model) -> Result<(), MegaError> { - let a_model = mr.into_active_model(); - a_model.insert(self.get_connection()).await.unwrap(); - Ok(()) + pub async fn new_mr( + &self, + path: &str, + title: &str, + from_hash: &str, + to_hash: &str, + ) -> Result { + let link = common::utils::generate_link(); + + let mr = mega_mr::ActiveModel { + id: Set(generate_id()), + link: Set(link.clone()), + title: Set(title.to_owned()), + merge_date: Set(None), + status: Set(MergeStatusEnum::Open), + path: Set(path.to_owned()), + from_hash: Set(from_hash.to_owned()), + to_hash: Set(to_hash.to_owned()), + created_at: Set(chrono::Utc::now().naive_utc()), + updated_at: Set(chrono::Utc::now().naive_utc()), + }; + + mr.insert(self.get_connection()).await.unwrap(); + Ok(link) } - pub async fn close_mr( - &self, - model: mega_mr::Model, - user_id: String, - username: &str, - ) -> Result<(), MegaError> { - self.update_mr(model.clone()).await.unwrap(); - self.add_mr_conversation( - &model.link, - user_id, - ConvTypeEnum::Closed, - Some(format!("{} closed this", username)), - ) - .await - .unwrap(); + pub async fn close_mr(&self, model: mega_mr::Model) -> Result<(), MegaError> { + let mut a_model = model.into_active_model(); + a_model.status = Set(MergeStatusEnum::Closed); + a_model.updated_at = Set(chrono::Utc::now().naive_utc()); + a_model.update(self.get_connection()).await.unwrap(); Ok(()) } - pub async fn reopen_mr( - &self, - model: mega_mr::Model, - user_id: String, - username: &str, - ) -> Result<(), MegaError> { - self.update_mr(model.clone()).await.unwrap(); - self.add_mr_conversation( - &model.link, - user_id, - ConvTypeEnum::Reopen, - Some(format!("{} reopen this", username)), - ) - .await - .unwrap(); + pub async fn reopen_mr(&self, model: mega_mr::Model) -> Result<(), MegaError> { + let mut a_model = model.into_active_model(); + a_model.status = Set(MergeStatusEnum::Open); + a_model.updated_at = Set(chrono::Utc::now().naive_utc()); + a_model.update(self.get_connection()).await.unwrap(); Ok(()) } - pub async fn update_mr(&self, mr: mega_mr::Model) -> Result<(), MegaError> { - let mut a_model = mr.into_active_model(); - a_model = a_model.reset_all(); + pub async fn merge_mr(&self, model: mega_mr::Model) -> Result<(), MegaError> { + let mut a_model = model.into_active_model(); + a_model.status = Set(MergeStatusEnum::Merged); a_model.updated_at = Set(chrono::Utc::now().naive_utc()); a_model.update(self.get_connection()).await.unwrap(); Ok(()) } - pub async fn get_mr_conversations( + pub async fn update_mr_to_hash( &self, - link: &str, - ) -> Result, MegaError> { - let model = mega_conversation::Entity::find() - .filter(mega_conversation::Column::Link.eq(link)) - .all(self.get_connection()) - .await; - Ok(model?) - } - - pub async fn remove_mr_conversation(&self, id: i64) -> Result<(), MegaError> { - mega_conversation::Entity::delete_by_id(id) - .exec(self.get_connection()) - .await - .unwrap(); + model: mega_mr::Model, + to_hash: &str, + ) -> Result<(), MegaError> { + let mut a_model = model.into_active_model(); + a_model.to_hash = Set(to_hash.to_owned()); + a_model.updated_at = Set(chrono::Utc::now().naive_utc()); + a_model.update(self.get_connection()).await.unwrap(); Ok(()) } - pub async fn add_mr_conversation( - &self, - link: &str, - user_id: String, - conv_type: ConvTypeEnum, - comment: Option, - ) -> Result { - let conversation = mega_conversation::Model { - id: generate_id(), - link: link.to_owned(), - user_id, - conv_type, - comment, - created_at: chrono::Utc::now().naive_utc(), - updated_at: chrono::Utc::now().naive_utc(), - }; - let conversation = conversation.into_active_model(); - let res = conversation.insert(self.get_connection()).await.unwrap(); - Ok(res.id) - } } diff --git a/mono/src/api/api_router.rs b/mono/src/api/api_router.rs index ba56cb471..ac293cd1e 100644 --- a/mono/src/api/api_router.rs +++ b/mono/src/api/api_router.rs @@ -19,10 +19,9 @@ use ceres::{ use common::model::CommonResult; use utoipa_axum::{router::OpenApiRouter, routes}; -use crate::api::issue::issue_router; -use crate::api::mr::mr_router; -use crate::api::user::user_router; -use crate::api::MonoApiServiceState; +use crate::api::{ + issue::issue_router, label::label_router, mr::mr_router, user::user_router, MonoApiServiceState, +}; use crate::{api::error::ApiError, server::https_server::GIT_TAG}; pub fn routers() -> OpenApiRouter { @@ -41,6 +40,7 @@ pub fn routers() -> OpenApiRouter { .merge(mr_router::routers()) .merge(user_router::routers()) .merge(issue_router::routers()) + .merge(label_router::routers()) } /// Get blob file as string diff --git a/mono/src/api/error.rs b/mono/src/api/error.rs index cdf10669c..1c83e2c9b 100644 --- a/mono/src/api/error.rs +++ b/mono/src/api/error.rs @@ -1,4 +1,5 @@ -use axum::response::{IntoResponse, Response}; +use axum::response::{IntoResponse, Json, Response}; +use common::model::CommonResult; use http::StatusCode; #[derive(Debug)] @@ -8,7 +9,8 @@ impl IntoResponse for ApiError { fn into_response(self) -> Response { tracing::error!("Application error: {:#}", self.0); - (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response() + let body = Json(CommonResult::<()>::common_failed()); + (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() } } diff --git a/mono/src/api/issue/issue_router.rs b/mono/src/api/issue/issue_router.rs index d1da7e550..e9d797032 100644 --- a/mono/src/api/issue/issue_router.rs +++ b/mono/src/api/issue/issue_router.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use axum::{ extract::{Path, State}, Json, @@ -6,10 +8,11 @@ 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::mr::SaveCommentRequest; use crate::api::MonoApiServiceState; +use crate::api::{issue::LabelUpdatePayload, mr::SaveCommentRequest}; use crate::api::{ issue::{IssueDetail, IssueItem, NewIssue}, oauth::model::LoginUser, @@ -26,7 +29,8 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(reopen_issue)) .routes(routes!(issue_detail)) .routes(routes!(save_comment)) - .routes(routes!(delete_comment)), + .routes(routes!(delete_comment)) + .routes(routes!(labels)), ) } @@ -49,19 +53,14 @@ async fn fetch_issue_list( state: State, Json(json): Json>, ) -> Result>>, ApiError> { - let res = state + let (items, total) = state .issue_stg() .get_issue_by_status(&json.additional.status, json.pagination) - .await; - let res = match res { - Ok((items, total)) => CommonResult::success(Some(CommonPage { - items: items.into_iter().map(|m| m.into()).collect(), - total, - })), - - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + .await?; + Ok(Json(CommonResult::success(Some(CommonPage { + items: items.into_iter().map(|m| m.into()).collect(), + total, + })))) } /// Get issue details @@ -80,18 +79,14 @@ async fn issue_detail( Path(link): Path, state: State, ) -> Result>, ApiError> { - let res = match state.issue_stg().get_issue(&link).await { - Ok(data) => { - if let Some(model) = data { - let mut detail: IssueDetail = model.into(); - let conversations = state.mr_stg().get_mr_conversations(&link).await.unwrap(); - detail.conversations = conversations.into_iter().map(|x| x.into()).collect(); - CommonResult::success(Some(detail)) - } else { - CommonResult::success(None) - } - } - Err(err) => CommonResult::failed(&err.to_string()), + let res = state.issue_stg().get_issue(&link).await?; + let res = if let Some(model) = res { + let mut detail: IssueDetail = model.into(); + let conversations = state.issue_stg().get_conversations(&link).await.unwrap(); + detail.conversations = conversations.into_iter().map(|x| x.into()).collect(); + CommonResult::success(Some(detail)) + } else { + CommonResult::success(None) }; Ok(Json(res)) } @@ -112,15 +107,19 @@ async fn new_issue( Json(json): Json, ) -> Result>, ApiError> { let stg = state.issue_stg().clone(); - let res = stg.save_issue(0, &json.title).await.unwrap(); let res = stg - .add_issue_conversation(&res.link, user.campsite_user_id, Some(json.description)) - .await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + .save_issue(&user.campsite_user_id, &json.title) + .await + .unwrap(); + let _ = stg + .add_conversation( + &res.link, + &user.campsite_user_id, + Some(json.description), + ConvTypeEnum::Comment, + ) + .await?; + Ok(Json(CommonResult::success(None))) } /// Close an issue @@ -136,15 +135,12 @@ async fn new_issue( tag = ISSUE_TAG )] async fn close_issue( - // _: LoginUser, + _: LoginUser, Path(link): Path, state: State, ) -> Result>, ApiError> { - let res = match state.issue_stg().close_issue(&link).await { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + state.issue_stg().close_issue(&link).await?; + Ok(Json(CommonResult::success(None))) } /// Reopen an issue @@ -160,15 +156,12 @@ async fn close_issue( tag = ISSUE_TAG )] async fn reopen_issue( - // _: LoginUser, + _: LoginUser, Path(link): Path, state: State, ) -> Result>, ApiError> { - let res = match state.issue_stg().reopen_issue(&link).await { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + state.issue_stg().reopen_issue(&link).await?; + Ok(Json(CommonResult::success(None))) } /// Add new comment on Issue @@ -190,15 +183,16 @@ async fn save_comment( state: State, Json(payload): Json, ) -> Result>, ApiError> { - let res = match state + state .issue_stg() - .add_issue_conversation(&link, user.campsite_user_id, Some(payload.content)) - .await - { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + .add_conversation( + &link, + &user.campsite_user_id, + Some(payload.content), + ConvTypeEnum::Comment, + ) + .await?; + Ok(Json(CommonResult::success(None))) } /// Delete Issue Comment @@ -218,10 +212,54 @@ async fn delete_comment( Path(id): Path, state: State, ) -> Result>, ApiError> { - let res = state.issue_stg().remove_issue_conversation(id).await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + state.issue_stg().remove_conversation(id).await?; + Ok(Json(CommonResult::success(None))) +} + +/// update issue related labels +#[utoipa::path( + post, + path = "/labels", + request_body = LabelUpdatePayload, + responses( + (status = 200, body = CommonResult, content_type = "application/json") + ), + tag = ISSUE_TAG +)] +async fn labels( + user: LoginUser, + state: State, + Json(payload): Json, +) -> Result>, ApiError> { + common_label_update(user, state, payload).await +} + +pub async fn common_label_update( + user: LoginUser, + state: State, + payload: LabelUpdatePayload, +) -> 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.campsite_user_id, item_id, &link, to_add, to_remove) + .await?; + Ok(Json(CommonResult::success(None))) } diff --git a/mono/src/api/issue/mod.rs b/mono/src/api/issue/mod.rs index 18f401bb4..eeb3320c4 100644 --- a/mono/src/api/issue/mod.rs +++ b/mono/src/api/issue/mod.rs @@ -2,7 +2,7 @@ use callisto::mega_issue; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::api::mr::MegaConversation; +use crate::api::{label::LabelItem, mr::MegaConversation}; pub mod issue_router; @@ -11,7 +11,8 @@ pub struct IssueItem { pub link: String, pub title: String, pub status: String, - pub owner: i64, + pub user_id: String, + pub labels: Vec, pub open_timestamp: i64, pub closed_at: Option, pub updated_at: i64, @@ -23,10 +24,11 @@ impl From for IssueItem { link: value.link, title: value.title, status: value.status.to_string(), - owner: value.owner, + user_id: value.user_id, open_timestamp: value.created_at.and_utc().timestamp(), closed_at: value.closed_at.map(|dt| dt.and_utc().timestamp()), updated_at: value.updated_at.and_utc().timestamp(), + labels: vec![] } } } @@ -59,3 +61,10 @@ impl From for IssueDetail { } } } + +#[derive(Deserialize, ToSchema)] +pub struct LabelUpdatePayload { + label_ids: Vec, + item_id: i64, + link: String +} diff --git a/mono/src/api/label/label_router.rs b/mono/src/api/label/label_router.rs new file mode 100644 index 000000000..011afae64 --- /dev/null +++ b/mono/src/api/label/label_router.rs @@ -0,0 +1,65 @@ +use axum::{extract::State, Json}; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use common::model::{CommonPage, CommonResult, PageParams}; + +use crate::api::label::{LabelItem, NewLabel}; +use crate::api::oauth::model::LoginUser; +use crate::api::MonoApiServiceState; +use crate::{api::error::ApiError, server::https_server::LABEL_TAG}; + +pub fn routers() -> OpenApiRouter { + OpenApiRouter::new().nest( + "/label", + OpenApiRouter::new() + .routes(routes!(new_label)) + .routes(routes!(fetch_label_list)), + ) +} + +/// List label in page +#[utoipa::path( + post, + path = "/list", + request_body = PageParams, + responses( + (status = 200, body = CommonResult>, content_type = "application/json") + ), + tag = LABEL_TAG +)] +async fn fetch_label_list( + _user: LoginUser, + state: State, + Json(json): Json>, +) -> Result>>, ApiError> { + let (items, total) = state + .issue_stg() + .list_labels_by_page(json.pagination) + .await?; + Ok(Json(CommonResult::success(Some(CommonPage { + items: items.into_iter().map(|m| m.into()).collect(), + total, + })))) +} + +/// New label +#[utoipa::path( + post, + path = "/new", + request_body = NewLabel, + responses( + (status = 200, body = CommonResult, content_type = "application/json") + ), + tag = LABEL_TAG +)] +async fn new_label( + _user: LoginUser, + state: State, + Json(json): Json, +) -> Result>, ApiError> { + let stg = state.issue_stg().clone(); + let res = stg + .new_label(&json.name, &json.color, &json.description) + .await?; + Ok(Json(CommonResult::success(Some(res.into())))) +} diff --git a/mono/src/api/label/mod.rs b/mono/src/api/label/mod.rs new file mode 100644 index 000000000..c19bb3b5b --- /dev/null +++ b/mono/src/api/label/mod.rs @@ -0,0 +1,31 @@ +pub mod label_router; + +use callisto::label; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct LabelItem { + pub id: i64, + pub name: String, + pub color: String, + pub description: String, +} + +impl From for LabelItem { + fn from(value: label::Model) -> Self { + Self { + id: value.id, + name: value.name, + color: value.color, + description: value.description, + } + } +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct NewLabel { + pub name: String, + pub color: String, + pub description: String, +} diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index faef204a7..6c9569754 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -26,6 +26,7 @@ use crate::api::oauth::campsite_store::CampsiteApiStore; pub mod api_router; pub mod error; pub mod issue; +pub mod label; pub mod lfs; pub mod mr; pub mod oauth; diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index b81041be5..d816f4754 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -8,15 +8,16 @@ use saturn::ActionEnum; use utoipa_axum::{router::OpenApiRouter, routes}; use callisto::sea_orm_active_enums::{ConvTypeEnum, MergeStatusEnum}; -use ceres::protocol::mr::MergeRequest; -use common::model::{CommonPage, CommonResult, PageParams}; +use common::{ + errors::MegaError, + model::{CommonPage, CommonResult, PageParams}, +}; use crate::api::{ - mr::{ + issue::{issue_router::common_label_update, LabelUpdatePayload}, mr::{ FilesChangedItem, FilesChangedList, MRDetail, MRStatusParams, MrInfoItem, SaveCommentRequest, - }, - oauth::model::LoginUser, + }, oauth::model::LoginUser }; use crate::api::{util, MonoApiServiceState}; use crate::{api::error::ApiError, server::https_server::MR_TAG}; @@ -32,7 +33,8 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(reopen_mr)) .routes(routes!(get_mr_files_changed)) .routes(routes!(save_comment)) - .routes(routes!(delete_comment)), + .routes(routes!(delete_comment)) + .routes(routes!(labels)), ) } @@ -53,30 +55,33 @@ async fn reopen_mr( Path(link): Path, state: State, ) -> Result>, ApiError> { - if let Some(model) = state.mr_stg().get_mr(&link).await.unwrap() { - if model.status == MergeStatusEnum::Closed { - // util::check_permissions( - // &user.name, - // &model.path, - // ActionEnum::EditMergeRequest, - // state.clone(), - // ) - // .await - // .unwrap(); - let mut mr: MergeRequest = model.into(); - mr.status = MergeStatusEnum::Open; - let res = match state - .mr_stg() - .reopen_mr(mr.into(), user.campsite_user_id, &user.name) - .await - { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - return Ok(Json(res)); - } + let res = state.mr_stg().get_mr(&link).await?; + let model = res.ok_or(MegaError::with_message("Not Found"))?; + + if model.status == MergeStatusEnum::Closed { + // util::check_permissions( + // &user.name, + // &model.path, + // ActionEnum::EditMergeRequest, + // state.clone(), + // ) + // .await + // .unwrap(); + + let link = model.link.clone(); + state.mr_stg().reopen_mr(model).await?; + state + .issue_stg() + .add_conversation( + &link, + &user.campsite_user_id, + Some(format!("{} reopen this", user.name)), + ConvTypeEnum::Reopen, + ) + .await + .unwrap(); } - Ok(Json(CommonResult::failed("not found"))) + Ok(Json(CommonResult::success(None))) } /// Close Merge Request @@ -96,30 +101,31 @@ async fn close_mr( Path(link): Path, state: State, ) -> Result>, ApiError> { - if let Some(model) = state.mr_stg().get_mr(&link).await.unwrap() { - if model.status == MergeStatusEnum::Open { - // util::check_permissions( - // &user.name, - // &model.path, - // ActionEnum::EditMergeRequest, - // state.clone(), - // ) - // .await - // .unwrap(); - let mut mr: MergeRequest = model.into(); - mr.status = MergeStatusEnum::Closed; - let res = match state - .mr_stg() - .close_mr(mr.into(), user.campsite_user_id, &user.name) - .await - { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - return Ok(Json(res)); - } + let res = state.mr_stg().get_mr(&link).await?; + let model = res.ok_or(MegaError::with_message("Not Found"))?; + + if model.status == MergeStatusEnum::Open { + // util::check_permissions( + // &user.name, + // &model.path, + // ActionEnum::EditMergeRequest, + // state.clone(), + // ) + // .await + // .unwrap(); + let link = model.link.clone(); + state.mr_stg().close_mr(model).await?; + state + .issue_stg() + .add_conversation( + &link, + &user.campsite_user_id, + Some(format!("{} closed this", user.name)), + ConvTypeEnum::Closed, + ) + .await?; } - Ok(Json(CommonResult::failed("not found"))) + Ok(Json(CommonResult::success(None))) } /// Approve Merge Request @@ -139,29 +145,25 @@ async fn merge( Path(link): Path, state: State, ) -> Result>, ApiError> { - if let Some(model) = state.mr_stg().get_mr(&link).await.unwrap() { - if model.status == MergeStatusEnum::Open { - let path = model.path.clone(); - util::check_permissions( - &user.name, - &path, - ActionEnum::ApproveMergeRequest, - state.clone(), - ) - .await - .unwrap(); - let res = state - .monorepo() - .merge_mr(user.campsite_user_id, &mut model.into()) - .await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - return Ok(Json(res)); - } + let res = state.mr_stg().get_mr(&link).await?; + let model = res.ok_or(MegaError::with_message("Not Found"))?; + + if model.status == MergeStatusEnum::Open { + let path = model.path.clone(); + util::check_permissions( + &user.name, + &path, + ActionEnum::ApproveMergeRequest, + state.clone(), + ) + .await + .unwrap(); + state + .monorepo() + .merge_mr(user.campsite_user_id, model) + .await?; } - Ok(Json(CommonResult::failed("not found"))) + Ok(Json(CommonResult::success(None))) } /// Fetch MR list @@ -190,17 +192,14 @@ async fn fetch_mr_list( MergeStatusEnum::Merged, ] }; - let res = match state + let (items, total) = state .mr_stg() .get_mr_by_status(status, json.pagination.page, json.pagination.per_page) - .await - { - Ok((items, total)) => CommonResult::success(Some(CommonPage { - items: items.into_iter().map(|m| m.into()).collect(), - total, - })), - Err(err) => CommonResult::failed(&err.to_string()), - }; + .await?; + let res = CommonResult::success(Some(CommonPage { + items: items.into_iter().map(|m| m.into()).collect(), + total, + })); Ok(Json(res)) } @@ -220,19 +219,12 @@ async fn mr_detail( Path(link): Path, state: State, ) -> Result>, ApiError> { - let res = match state.mr_stg().get_mr(&link).await { - Ok(data) => { - if let Some(model) = data { - let mut detail: MRDetail = model.into(); - let conversations = state.mr_stg().get_mr_conversations(&link).await.unwrap(); - detail.conversations = conversations.into_iter().map(|x| x.into()).collect(); - CommonResult::success(Some(detail)) - } else { - CommonResult::success(None) - } - } - Err(err) => CommonResult::failed(&err.to_string()), - }; + let res = state.mr_stg().get_mr(&link).await?; + let model = res.ok_or(MegaError::with_message("MR Not Found"))?; + let mut detail: MRDetail = model.into(); + let conversations = state.issue_stg().get_conversations(&link).await.unwrap(); + detail.conversations = conversations.into_iter().map(|x| x.into()).collect(); + let res = CommonResult::success(Some(detail)); Ok(Json(res)) } @@ -253,23 +245,17 @@ async fn get_mr_files_changed( state: State, ) -> Result>, ApiError> { let listen_addr = &state.listen_addr; - let res = state.monorepo().content_diff(&link, listen_addr).await; - let res = match res { - Ok(data) => { - let diff_files = extract_files_with_status(&data); - let mut diff_list: Vec = vec![]; - for (path, status) in diff_files { - diff_list.push(FilesChangedItem { path, status }); - } - - CommonResult::success(Some(FilesChangedList { - files: diff_list, - content: data, - })) - } - Err(err) => CommonResult::failed(&err.to_string()), - }; + let diff_res = state.monorepo().content_diff(&link, listen_addr).await?; + let diff_files = extract_files_with_status(&diff_res); + let mut diff_list: Vec = vec![]; + for (path, status) in diff_files { + diff_list.push(FilesChangedItem { path, status }); + } + let res = CommonResult::success(Some(FilesChangedList { + files: diff_list, + content: diff_res, + })); Ok(Json(res)) } @@ -292,22 +278,18 @@ async fn save_comment( state: State, Json(payload): Json, ) -> Result>, ApiError> { - let res = if let Some(model) = state.mr_stg().get_mr(&link).await.unwrap() { - state - .mr_stg() - .add_mr_conversation( - &model.link, - user.campsite_user_id, - ConvTypeEnum::Comment, - Some(payload.content), - ) - .await - .unwrap(); - CommonResult::success(None) - } else { - CommonResult::failed("Invalid link") - }; - Ok(Json(res)) + let res = state.mr_stg().get_mr(&link).await?; + let model = res.ok_or(MegaError::with_message("Not Found"))?; + state + .issue_stg() + .add_conversation( + &model.link, + &user.campsite_user_id, + Some(payload.content), + ConvTypeEnum::Comment, + ) + .await?; + Ok(Json(CommonResult::success(None))) } /// Delete Comment @@ -326,11 +308,8 @@ async fn delete_comment( Path(conv_id): Path, state: State, ) -> Result>, ApiError> { - let res = match state.mr_stg().remove_mr_conversation(conv_id).await { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + state.issue_stg().remove_conversation(conv_id).await?; + Ok(Json(CommonResult::success(None))) } fn extract_files_with_status(diff_output: &str) -> HashMap { @@ -353,6 +332,24 @@ fn extract_files_with_status(diff_output: &str) -> HashMap { files } +/// update mr related labels +#[utoipa::path( + post, + path = "/labels", + request_body = LabelUpdatePayload, + responses( + (status = 200, body = CommonResult, content_type = "application/json") + ), + tag = MR_TAG +)] +async fn labels( + user: LoginUser, + state: State, + Json(payload): Json, +) -> Result>, ApiError> { + common_label_update(user, state, payload).await +} + #[cfg(test)] mod test { use std::collections::HashMap; diff --git a/mono/src/api/oauth/campsite_store.rs b/mono/src/api/oauth/campsite_store.rs index c16fc4d83..81480d451 100644 --- a/mono/src/api/oauth/campsite_store.rs +++ b/mono/src/api/oauth/campsite_store.rs @@ -37,8 +37,7 @@ impl SessionStore for CampsiteApiStore { // println!("Raw response: {}", text); let campsite_user = resp .json::() - .await - .context("failed to deserialize response as JSON")?; + .await?; let login_user: LoginUser = campsite_user.into(); session .insert("user", &login_user) diff --git a/mono/src/api/oauth/mod.rs b/mono/src/api/oauth/mod.rs index b3881f01b..6a9daf18c 100644 --- a/mono/src/api/oauth/mod.rs +++ b/mono/src/api/oauth/mod.rs @@ -226,7 +226,10 @@ where let session = store .load_session(session_cookie.to_string()) .await - .unwrap() + .map_err(|e| { + tracing::error!("load_session error: {:?}", e); + AuthRedirect + })? .ok_or(AuthRedirect)?; let user = session.get::("user").ok_or(AuthRedirect)?; diff --git a/mono/src/api/user/user_router.rs b/mono/src/api/user/user_router.rs index eeb41d5ba..1782ea967 100644 --- a/mono/src/api/user/user_router.rs +++ b/mono/src/api/user/user_router.rs @@ -56,7 +56,7 @@ async fn add_key( .to_owned() }; - let res = state + state .user_stg() .save_ssh_key( user.campsite_user_id, @@ -64,12 +64,8 @@ async fn add_key( &json.ssh_key, &key.fingerprint(HashAlg::Sha256).to_string(), ) - .await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + .await?; + Ok(Json(CommonResult::success(None))) } async fn remove_key( @@ -77,39 +73,35 @@ async fn remove_key( state: State, Path(key_id): Path, ) -> Result>, ApiError> { - let res = state + state .user_stg() .delete_ssh_key(user.campsite_user_id, key_id) - .await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + .await?; + Ok(Json(CommonResult::success(None))) } async fn list_key( user: LoginUser, state: State, ) -> Result>>, ApiError> { - let res = state.user_stg().list_user_ssh(user.campsite_user_id).await; - let res = match res { - Ok(data) => CommonResult::success(Some(data.into_iter().map(|x| x.into()).collect())), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + let res = state + .user_stg() + .list_user_ssh(user.campsite_user_id) + .await?; + Ok(Json(CommonResult::success(Some( + res.into_iter().map(|x| x.into()).collect(), + )))) } async fn generate_token( user: LoginUser, state: State, ) -> Result>, ApiError> { - let res = state.user_stg().generate_token(user.campsite_user_id).await; - let res = match res { - Ok(data) => CommonResult::success(Some(data)), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + let res = state + .user_stg() + .generate_token(user.campsite_user_id) + .await?; + Ok(Json(CommonResult::success(Some(res)))) } async fn remove_token( @@ -117,30 +109,20 @@ async fn remove_token( state: State, Path(key_id): Path, ) -> Result>, ApiError> { - let res = state + state .user_stg() .delete_token(user.campsite_user_id, key_id) - .await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + .await?; + Ok(Json(CommonResult::success(None))) } async fn list_token( user: LoginUser, state: State, ) -> Result>>, ApiError> { - let res = state.user_stg().list_token(user.campsite_user_id).await; - let res = match res { - Ok(data) => { - let res = data.into_iter().map(|x| x.into()).collect(); - CommonResult::success(Some(res)) - } - Err(err) => CommonResult::failed(&err.to_string()), - }; - Ok(Json(res)) + let data = state.user_stg().list_token(user.campsite_user_id).await?; + let res = data.into_iter().map(|x| x.into()).collect(); + Ok(Json(CommonResult::success(Some(res)))) } async fn repo_permissions( diff --git a/mono/src/server/https_server.rs b/mono/src/server/https_server.rs index 9c3edfe65..04f886353 100644 --- a/mono/src/server/https_server.rs +++ b/mono/src/server/https_server.rs @@ -127,7 +127,7 @@ pub async fn app(context: Context, host: String, port: u16) -> Router { http::header::AUTHORIZATION, http::header::CONTENT_TYPE, ]) - .allow_methods([Method::GET, Method::POST, Method::OPTIONS, Method::DELETE]) + .allow_methods([Method::GET, Method::POST, Method::OPTIONS, Method::DELETE, Method::PUT]) .allow_credentials(true), ), ) @@ -192,9 +192,11 @@ pub async fn post_method_router( } } +/// Swagger API tag pub const GIT_TAG: &str = "git"; pub const MR_TAG: &str = "merge_request"; pub const ISSUE_TAG: &str = "issue"; +pub const LABEL_TAG: &str = "label"; #[derive(OpenApi)] #[openapi( tags( diff --git a/moon/apps/web/components/Issues/IssuePage.tsx b/moon/apps/web/components/Issues/IssuePage.tsx index 37231d49d..c8211feea 100644 --- a/moon/apps/web/components/Issues/IssuePage.tsx +++ b/moon/apps/web/components/Issues/IssuePage.tsx @@ -16,7 +16,7 @@ import { apiErrorToast } from '@/utils/apiErrorToast' interface Item { closed_at?: number | null link: string - owner: number + user_id: string title: string status: string open_timestamp: number diff --git a/moon/apps/web/components/Issues/IssuesContent.tsx b/moon/apps/web/components/Issues/IssuesContent.tsx index c2850b239..8bb3a230a 100644 --- a/moon/apps/web/components/Issues/IssuesContent.tsx +++ b/moon/apps/web/components/Issues/IssuesContent.tsx @@ -40,7 +40,7 @@ interface Props { export interface Item { closed_at?: number | null link: string - owner: number + user_id: string title: string status: string open_timestamp: number @@ -314,7 +314,7 @@ export function IssuesContent({ searching }: Props) { rightIcon={} >
- {i.link} · {i.owner} {i.status}{' '} + {i.link} · {i.user_id} {i.status}{' '} {formatDistance(fromUnixTime(i.open_timestamp), new Date(), { addSuffix: true })}
diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index 862243ccd..3a0bfa907 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -3038,15 +3038,34 @@ export type CommonResultCommonPageIssueItem = { items: { /** @format int64 */ closed_at?: number | null + labels: LabelItem[] link: string /** @format int64 */ open_timestamp: number - /** @format int64 */ - owner: number status: string title: string /** @format int64 */ updated_at: number + user_id: string + }[] + /** + * @format int64 + * @min 0 + */ + total: number + } + err_message: string + req_result: boolean +} + +export type CommonResultCommonPageLabelItem = { + data?: { + items: { + color: string + description: string + /** @format int64 */ + id: number + name: string }[] /** * @format int64 @@ -3162,7 +3181,8 @@ export enum ConvTypeEnum { MergeQueue = 'MergeQueue', Merged = 'Merged', Closed = 'Closed', - Reopen = 'Reopen' + Reopen = 'Reopen', + Label = 'Label' } export type CreateFileInfo = { @@ -3187,15 +3207,30 @@ export type FilesChangedList = { export type IssueItem = { /** @format int64 */ closed_at?: number | null + labels: LabelItem[] link: string /** @format int64 */ open_timestamp: number - /** @format int64 */ - owner: number status: string title: string /** @format int64 */ updated_at: number + user_id: string +} + +export type LabelItem = { + color: string + description: string + /** @format int64 */ + id: number + name: string +} + +export type LabelUpdatePayload = { + /** @format int64 */ + item_id: number + label_ids: number[] + link: string } export type LatestCommitInfo = { @@ -3259,6 +3294,12 @@ export type NewIssue = { title: string } +export type NewLabel = { + color: string + description: string + name: string +} + export type PageParamsMRStatusParams = { additional: { status: string @@ -3273,6 +3314,11 @@ export type PageParamsStatusParams = { pagination: Pagination } +export type PageParamsString = { + additional: string + pagination: Pagination +} + export type Pagination = { /** * @format int64 @@ -4462,6 +4508,8 @@ export type PostApiCreateFileData = CommonResultString export type DeleteApiIssueCommentDeleteData = CommonResultString +export type PostApiIssueLabelsData = CommonResultString + export type PostApiIssueListData = CommonResultCommonPageIssueItem export type PostApiIssueNewData = CommonResultString @@ -4474,6 +4522,10 @@ export type GetApiIssueDetailData = CommonResultString export type PostApiIssueReopenData = CommonResultString +export type PostApiLabelListData = CommonResultCommonPageLabelItem + +export type PostApiLabelNewData = CommonResultString + export type GetApiLatestCommitParams = { refs?: string path?: string @@ -4483,6 +4535,8 @@ export type GetApiLatestCommitData = LatestCommitInfo export type DeleteApiMrCommentDeleteData = CommonResultString +export type PostApiMrLabelsData = CommonResultString + export type PostApiMrListData = CommonResultCommonPageMrInfoItem export type PostApiMrCloseData = CommonResultString @@ -12884,6 +12938,31 @@ export class Api extends HttpClient { + const base = 'POST:/api/v1/issue/labels' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: LabelUpdatePayload, params: RequestParams = {}) => + this.request({ + path: `/api/v1/issue/labels`, + method: 'POST', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + /** * No description * @@ -13028,6 +13107,56 @@ export class Api extends HttpClient { + const base = 'POST:/api/v1/label/list' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: PageParamsString, params: RequestParams = {}) => + this.request({ + path: `/api/v1/label/list`, + method: 'POST', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + + /** + * No description + * + * @tags label + * @name PostApiLabelNew + * @summary New label + * @request POST:/api/v1/label/new + */ + postApiLabelNew: () => { + const base = 'POST:/api/v1/label/new' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: NewLabel, params: RequestParams = {}) => + this.request({ + path: `/api/v1/label/new`, + method: 'POST', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + /** * No description * @@ -13075,6 +13204,31 @@ export class Api extends HttpClient { + const base = 'POST:/api/v1/mr/labels' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: LabelUpdatePayload, params: RequestParams = {}) => + this.request({ + path: `/api/v1/mr/labels`, + method: 'POST', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + /** * No description * diff --git a/third-party/Cargo.toml b/third-party/Cargo.toml index d40da86eb..4d8d370cd 100644 --- a/third-party/Cargo.toml +++ b/third-party/Cargo.toml @@ -99,8 +99,8 @@ mimalloc = "0.1.43" assert_cmd = "2.0.16" async-session = "3.0.0" dotenvy = "0.15.7" -tokio-tungstenite = "0.26.2" -tungstenite = "0.26.2" +tokio-tungstenite = "0.27.0" +tungstenite = "0.27.0" itertools = "0.14.0" rusty_vault = "0.2.1" bs58 = "0.5.1"