Skip to content
Clone

feat(issues): phase3d number addressing, bulk ops, time logging CRUD, dependents, pin reorder, comment tree

ZhenYi committed
parent b8f89bc commit 61e78d8 Browse files

69 files changed

+767 −68

File tree 69
Modified lib/issues/src/handlers/comments/create_comment.rs View lib/issues/src/handlers/comments/create_comment.rs
@@ -29,8 +29,9 @@
2929 Json(cmd): Json<CreateCommentCmd>,
3030) -> Result<AppResponseData<CommentDto>, ApiError> {
3131 let uid = require_uid(&session)?;
32 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3233 let dto = service
33 .create_comment(uid, &path.space_name, path.board_id, path.issue_id, cmd)
34 .create_comment(uid, &path.space_name, path.board_id, issue_id, cmd)
3435 .await?;
3536 Ok(AppResponseData::ok(dto))
3637}
Modified lib/issues/src/handlers/comments/delete_comment.rs View lib/issues/src/handlers/comments/delete_comment.rs
@@ -24,12 +24,13 @@
2424 Path(path): Path<CommentPath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
2829 .delete_comment(
2930 uid,
3031 &path.space_name,
3132 path.board_id,
32 path.issue_id,
33 issue_id,
3334 path.comment_id,
3435 )
3536 .await?;
Modified lib/issues/src/handlers/comments/get_comment.rs View lib/issues/src/handlers/comments/get_comment.rs
@@ -24,12 +24,13 @@
2424 Path(path): Path<CommentPath>,
2525) -> Result<AppResponseData<CommentDto>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 let dto = service
2829 .get_comment(
2930 uid,
3031 &path.space_name,
3132 path.board_id,
32 path.issue_id,
33 issue_id,
3334 path.comment_id,
3435 )
3536 .await?;
Modified lib/issues/src/handlers/comments/list_comments.rs View lib/issues/src/handlers/comments/list_comments.rs
@@ -26,8 +26,9 @@
2626 Query(q): Query<ListCommentQuery>,
2727) -> Result<AppResponseData<CommentPage>, ApiError> {
2828 let uid = require_uid(&session)?;
29 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2930 let page = service
30 .list_comments(uid, &path.space_name, path.board_id, path.issue_id, q)
31 .list_comments(uid, &path.space_name, path.board_id, issue_id, q)
3132 .await?;
3233 Ok(AppResponseData::ok(page))
3334}
Added lib/issues/src/handlers/comments/list_comments_tree.rs View lib/issues/src/handlers/comments/list_comments_tree.rs
@@ -0,0 +1,34 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4
5use crate::errors::ApiError;
6use crate::services::comments::CommentTreeNode;
7use crate::services::issues::IssuePath;
8use crate::services::service::IssueService;
9use session::share::session::require_uid;
10
11#[utoipa::path(
12 get,
13 path = "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/comments/tree",
14 tag = "issues",
15 params(IssuePath),
16 responses(
17 (status = 200, description = "nested comment tree", body = AppResponseData<Vec<CommentTreeNode>>),
18 (status = 404, description = "not found", body = AppResponseData<NoData>)
19 )
20)]
21pub async fn list_comments_tree(
22 State(service): State<IssueService>,
23 session: Session,
24 Path(path): Path<IssuePath>,
25) -> Result<AppResponseData<Vec<CommentTreeNode>>, ApiError> {
26 let uid = require_uid(&session)?;
27 let issue_id = service
28 .resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id)
29 .await?;
30 let tree = service
31 .list_comments_tree(uid, &path.space_name, path.board_id, issue_id)
32 .await?;
33 Ok(AppResponseData::ok(tree))
34}
Modified lib/issues/src/handlers/comments/mod.rs View lib/issues/src/handlers/comments/mod.rs
@@ -3,3 +3,4 @@
33pub mod get_comment;
44pub mod list_comments;
55pub mod update_comment;
6pub mod list_comments_tree;
Modified lib/issues/src/handlers/comments/update_comment.rs View lib/issues/src/handlers/comments/update_comment.rs
@@ -27,12 +27,13 @@
2727 Json(cmd): Json<UpdateCommentCmd>,
2828) -> Result<AppResponseData<CommentDto>, ApiError> {
2929 let uid = require_uid(&session)?;
30 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3031 let dto = service
3132 .update_comment(
3233 uid,
3334 &path.space_name,
3435 path.board_id,
35 path.issue_id,
36 issue_id,
3637 path.comment_id,
3738 cmd,
3839 )
Modified lib/issues/src/handlers/content_history/list_content_history.rs View lib/issues/src/handlers/content_history/list_content_history.rs
@@ -27,8 +27,9 @@
2727 Query(q): Query<ListContentHistoryQuery>,
2828) -> Result<AppResponseData<ContentHistoryPage>, ApiError> {
2929 let uid = require_uid(&session)?;
30 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3031 let page = service
31 .list_content_history(uid, &path.space_name, path.board_id, path.issue_id, q)
32 .list_content_history(uid, &path.space_name, path.board_id, issue_id, q)
3233 .await?;
3334 Ok(AppResponseData::ok(page))
3435}
Modified lib/issues/src/handlers/dependencies/create_dependency.rs View lib/issues/src/handlers/dependencies/create_dependency.rs
@@ -29,8 +29,9 @@
2929 Json(cmd): Json<CreateDependencyCmd>,
3030) -> Result<AppResponseData<DependencyDto>, ApiError> {
3131 let uid = require_uid(&session)?;
32 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3233 let dto = service
33 .create_dependency(uid, &path.space_name, path.board_id, path.issue_id, cmd)
34 .create_dependency(uid, &path.space_name, path.board_id, issue_id, cmd)
3435 .await?;
3536 Ok(AppResponseData::ok(dto))
3637}
Modified lib/issues/src/handlers/dependencies/delete_dependency.rs View lib/issues/src/handlers/dependencies/delete_dependency.rs
@@ -24,12 +24,13 @@
2424 Path(path): Path<DependencyPath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
2829 .delete_dependency(
2930 uid,
3031 &path.space_name,
3132 path.board_id,
32 path.issue_id,
33 issue_id,
3334 path.dep_id,
3435 )
3536 .await?;
Modified lib/issues/src/handlers/dependencies/list_dependencies.rs View lib/issues/src/handlers/dependencies/list_dependencies.rs
@@ -25,8 +25,9 @@
2525 Path(path): Path<IssuePath>,
2626) -> Result<AppResponseData<Vec<DependencyDto>>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 let deps = service
29 .list_dependencies(uid, &path.space_name, path.board_id, path.issue_id)
30 .list_dependencies(uid, &path.space_name, path.board_id, issue_id)
3031 .await?;
3132 Ok(AppResponseData::ok(deps))
3233}
Added lib/issues/src/handlers/dependencies/list_dependents.rs View lib/issues/src/handlers/dependencies/list_dependents.rs
@@ -0,0 +1,34 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4
5use crate::errors::ApiError;
6use crate::services::dependencies::DependencyDto;
7use crate::services::issues::IssuePath;
8use crate::services::service::IssueService;
9use session::share::session::require_uid;
10
11#[utoipa::path(
12 get,
13 path = "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/dependents",
14 tag = "issues",
15 params(IssuePath),
16 responses(
17 (status = 200, description = "issues depending on this one", body = AppResponseData<Vec<DependencyDto>>),
18 (status = 404, description = "not found", body = AppResponseData<NoData>)
19 )
20)]
21pub async fn list_dependents(
22 State(service): State<IssueService>,
23 session: Session,
24 Path(path): Path<IssuePath>,
25) -> Result<AppResponseData<Vec<DependencyDto>>, ApiError> {
26 let uid = require_uid(&session)?;
27 let issue_id = service
28 .resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id)
29 .await?;
30 let deps = service
31 .list_dependents(uid, &path.space_name, path.board_id, issue_id)
32 .await?;
33 Ok(AppResponseData::ok(deps))
34}
Modified lib/issues/src/handlers/dependencies/mod.rs View lib/issues/src/handlers/dependencies/mod.rs
@@ -1,3 +1,4 @@
11pub mod create_dependency;
22pub mod delete_dependency;
33pub mod list_dependencies;
4pub mod list_dependents;
Modified lib/issues/src/handlers/events/list_events.rs View lib/issues/src/handlers/events/list_events.rs
@@ -25,8 +25,9 @@
2525 Query(q): Query<ListEventQuery>,
2626) -> Result<AppResponseData<EventPage>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 let page = service
29 .list_events(uid, &path.space_name, path.board_id, path.issue_id, q)
30 .list_events(uid, &path.space_name, path.board_id, issue_id, q)
3031 .await?;
3132 Ok(AppResponseData::ok(page))
3233}
Modified lib/issues/src/handlers/issue_users/list_issue_users.rs View lib/issues/src/handlers/issue_users/list_issue_users.rs
@@ -26,8 +26,9 @@
2626 Query(q): Query<ListIssueUserQuery>,
2727) -> Result<AppResponseData<IssueUserPage>, ApiError> {
2828 let uid = require_uid(&session)?;
29 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2930 let page = service
30 .list_issue_users(uid, &path.space_name, path.board_id, path.issue_id, q)
31 .list_issue_users(uid, &path.space_name, path.board_id, issue_id, q)
3132 .await?;
3233 Ok(AppResponseData::ok(page))
3334}
Modified lib/issues/src/handlers/issue_users/update_issue_user.rs View lib/issues/src/handlers/issue_users/update_issue_user.rs
@@ -26,12 +26,13 @@
2626 Json(cmd): Json<UpdateIssueUserCmd>,
2727) -> Result<AppResponseData<IssueUserDto>, ApiError> {
2828 let caller = require_uid(&session)?;
29 let issue_id = service.resolve_issue_ref(caller, &path.space_name, path.board_id, &path.issue_id).await?;
2930 let dto = service
3031 .update_issue_user(
3132 caller,
3233 &path.space_name,
3334 path.board_id,
34 path.issue_id,
35 issue_id,
3536 path.user_id,
3637 cmd,
3738 )
Added lib/issues/src/handlers/issues/bulk_issues.rs View lib/issues/src/handlers/issues/bulk_issues.rs
@@ -0,0 +1,34 @@
1use axum::Json;
2use axum::extract::{Path, State};
3use export::rest::{AppResponseData, NoData};
4use session::Session;
5
6use crate::errors::ApiError;
7use crate::services::boards::BoardPath;
8use crate::services::issues::{BulkIssuesCmd, BulkResultDto};
9use crate::services::service::IssueService;
10use session::share::session::require_uid;
11
12#[utoipa::path(
13 post,
14 path = "/issues/{space_name}/boards/{board_id}/issues/bulk",
15 tag = "issues",
16 params(BoardPath),
17 request_body = BulkIssuesCmd,
18 responses(
19 (status = 200, description = "bulk result", body = AppResponseData<BulkResultDto>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
21 )
22)]
23pub async fn bulk_issues(
24 State(service): State<IssueService>,
25 session: Session,
26 Path(path): Path<BoardPath>,
27 Json(cmd): Json<BulkIssuesCmd>,
28) -> Result<AppResponseData<BulkResultDto>, ApiError> {
29 let uid = require_uid(&session)?;
30 let result = service
31 .bulk_update_state(uid, &path.space_name, path.board_id, cmd)
32 .await?;
33 Ok(AppResponseData::ok(result))
34}
Modified lib/issues/src/handlers/issues/close_issue.rs View lib/issues/src/handlers/issues/close_issue.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<IssuePath>,
2525) -> Result<AppResponseData<IssueDto>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 let dto = service
28 .close_issue(uid, &path.space_name, path.board_id, path.issue_id, true)
29 .close_issue(uid, &path.space_name, path.board_id, issue_id, true)
2930 .await?;
3031 Ok(AppResponseData::ok(dto))
3132}
Modified lib/issues/src/handlers/issues/delete_issue.rs View lib/issues/src/handlers/issues/delete_issue.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<IssuePath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
28 .delete_issue(uid, &path.space_name, path.board_id, path.issue_id)
29 .delete_issue(uid, &path.space_name, path.board_id, issue_id)
2930 .await?;
3031 Ok(AppResponseData::ok(NoData))
3132}
Modified lib/issues/src/handlers/issues/get_issue.rs View lib/issues/src/handlers/issues/get_issue.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<IssuePath>,
2525) -> Result<AppResponseData<IssueDto>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 let dto = service
28 .get_issue(uid, &path.space_name, path.board_id, path.issue_id)
29 .get_issue(uid, &path.space_name, path.board_id, issue_id)
2930 .await?;
3031 Ok(AppResponseData::ok(dto))
3132}
Modified lib/issues/src/handlers/issues/mod.rs View lib/issues/src/handlers/issues/mod.rs
@@ -5,3 +5,4 @@
55pub mod list_issues;
66pub mod reopen_issue;
77pub mod update_issue;
8pub mod bulk_issues;
Modified lib/issues/src/handlers/issues/reopen_issue.rs View lib/issues/src/handlers/issues/reopen_issue.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<IssuePath>,
2525) -> Result<AppResponseData<IssueDto>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 let dto = service
28 .close_issue(uid, &path.space_name, path.board_id, path.issue_id, false)
29 .close_issue(uid, &path.space_name, path.board_id, issue_id, false)
2930 .await?;
3031 Ok(AppResponseData::ok(dto))
3132}
Modified lib/issues/src/handlers/issues/update_issue.rs View lib/issues/src/handlers/issues/update_issue.rs
@@ -27,8 +27,9 @@
2727 Json(cmd): Json<UpdateIssueCmd>,
2828) -> Result<AppResponseData<IssueDto>, ApiError> {
2929 let uid = require_uid(&session)?;
30 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3031 let dto = service
31 .update_issue(uid, &path.space_name, path.board_id, path.issue_id, cmd)
32 .update_issue(uid, &path.space_name, path.board_id, issue_id, cmd)
3233 .await?;
3334 Ok(AppResponseData::ok(dto))
3435}
Modified lib/issues/src/handlers/labels/bind_label.rs View lib/issues/src/handlers/labels/bind_label.rs
@@ -25,12 +25,13 @@
2525 Path(path): Path<IssueLabelPath>,
2626) -> Result<AppResponseData<NoData>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 service
2930 .bind_label(
3031 uid,
3132 &path.space_name,
3233 path.board_id,
33 path.issue_id,
34 issue_id,
3435 path.label_id,
3536 )
3637 .await?;
Modified lib/issues/src/handlers/labels/list_labels_for_issue.rs View lib/issues/src/handlers/labels/list_labels_for_issue.rs
@@ -25,8 +25,9 @@
2525 Path(path): Path<IssuePath>,
2626) -> Result<AppResponseData<Vec<LabelDto>>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 let labels = service
29 .list_labels_for_issue(uid, &path.space_name, path.board_id, path.issue_id)
30 .list_labels_for_issue(uid, &path.space_name, path.board_id, issue_id)
3031 .await?;
3132 Ok(AppResponseData::ok(labels))
3233}
Modified lib/issues/src/handlers/labels/unbind_label.rs View lib/issues/src/handlers/labels/unbind_label.rs
@@ -24,12 +24,13 @@
2424 Path(path): Path<IssueLabelPath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
2829 .unbind_label(
2930 uid,
3031 &path.space_name,
3132 path.board_id,
32 path.issue_id,
33 issue_id,
3334 path.label_id,
3435 )
3536 .await?;
Modified lib/issues/src/handlers/members/create_member.rs View lib/issues/src/handlers/members/create_member.rs
@@ -29,8 +29,9 @@
2929 Json(cmd): Json<CreateIssueMemberCmd>,
3030) -> Result<AppResponseData<IssueMemberDto>, ApiError> {
3131 let uid = require_uid(&session)?;
32 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3233 let dto = service
33 .create_member(uid, &path.space_name, path.board_id, path.issue_id, cmd)
34 .create_member(uid, &path.space_name, path.board_id, issue_id, cmd)
3435 .await?;
3536 Ok(AppResponseData::ok(dto))
3637}
Modified lib/issues/src/handlers/members/delete_member.rs View lib/issues/src/handlers/members/delete_member.rs
@@ -24,12 +24,13 @@
2424 Path(path): Path<IssueMemberPath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
2829 .delete_member(
2930 uid,
3031 &path.space_name,
3132 path.board_id,
32 path.issue_id,
33 issue_id,
3334 path.member_id,
3435 )
3536 .await?;
Modified lib/issues/src/handlers/members/list_members.rs View lib/issues/src/handlers/members/list_members.rs
@@ -26,8 +26,9 @@
2626 Query(q): Query<ListIssueMemberQuery>,
2727) -> Result<AppResponseData<IssueMemberPage>, ApiError> {
2828 let uid = require_uid(&session)?;
29 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2930 let page = service
30 .list_members(uid, &path.space_name, path.board_id, path.issue_id, q)
31 .list_members(uid, &path.space_name, path.board_id, issue_id, q)
3132 .await?;
3233 Ok(AppResponseData::ok(page))
3334}
Modified lib/issues/src/handlers/pins/mod.rs View lib/issues/src/handlers/pins/mod.rs
@@ -1,3 +1,4 @@
11pub mod create_pin;
22pub mod delete_pin;
33pub mod list_pins;
4pub mod update_pin;
Added lib/issues/src/handlers/pins/update_pin.rs View lib/issues/src/handlers/pins/update_pin.rs
@@ -0,0 +1,33 @@
1use axum::Json;
2use axum::extract::{Path, State};
3use export::rest::{AppResponseData, NoData};
4use session::Session;
5
6use crate::errors::ApiError;
7use crate::services::pins::{PinDto, PinPath, UpdatePinCmd};
8use crate::services::service::IssueService;
9use session::share::session::require_uid;
10
11#[utoipa::path(
12 patch,
13 path = "/issues/{space_name}/boards/{board_id}/pins/{pin_id}",
14 tag = "issues",
15 params(PinPath),
16 request_body = UpdatePinCmd,
17 responses(
18 (status = 200, description = "pin updated", body = AppResponseData<PinDto>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
20 )
21)]
22pub async fn update_pin(
23 State(service): State<IssueService>,
24 session: Session,
25 Path(path): Path<PinPath>,
26 Json(cmd): Json<UpdatePinCmd>,
27) -> Result<AppResponseData<PinDto>, ApiError> {
28 let uid = require_uid(&session)?;
29 let dto = service
30 .update_pin(uid, &path.space_name, path.board_id, path.pin_id, cmd)
31 .await?;
32 Ok(AppResponseData::ok(dto))
33}
Modified lib/issues/src/handlers/reactions/add_comment_reaction.rs View lib/issues/src/handlers/reactions/add_comment_reaction.rs
@@ -28,12 +28,13 @@
2828 Json(cmd): Json<CreateReactionCmd>,
2929) -> Result<AppResponseData<ReactionDto>, ApiError> {
3030 let uid = require_uid(&session)?;
31 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3132 let dto = service
3233 .create_comment_reaction(
3334 uid,
3435 &path.space_name,
3536 path.board_id,
36 path.issue_id,
37 issue_id,
3738 path.comment_id,
3839 cmd,
3940 )
Modified lib/issues/src/handlers/reactions/add_reaction.rs View lib/issues/src/handlers/reactions/add_reaction.rs
@@ -29,8 +29,9 @@
2929 Json(cmd): Json<CreateReactionCmd>,
3030) -> Result<AppResponseData<ReactionDto>, ApiError> {
3131 let uid = require_uid(&session)?;
32 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3233 let dto = service
33 .create_reaction(uid, &path.space_name, path.board_id, path.issue_id, cmd)
34 .create_reaction(uid, &path.space_name, path.board_id, issue_id, cmd)
3435 .await?;
3536 Ok(AppResponseData::ok(dto))
3637}
Modified lib/issues/src/handlers/reactions/list_comment_reactions.rs View lib/issues/src/handlers/reactions/list_comment_reactions.rs
@@ -24,12 +24,13 @@
2424 Path(path): Path<CommentReactionPath>,
2525) -> Result<AppResponseData<Vec<ReactionAggDto>>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 let list = service
2829 .list_comment_reactions(
2930 uid,
3031 &path.space_name,
3132 path.board_id,
32 path.issue_id,
33 issue_id,
3334 path.comment_id,
3435 )
3536 .await?;
Modified lib/issues/src/handlers/reactions/list_reactions.rs View lib/issues/src/handlers/reactions/list_reactions.rs
@@ -25,8 +25,9 @@
2525 Path(path): Path<IssuePath>,
2626) -> Result<AppResponseData<Vec<ReactionAggDto>>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 let list = service
29 .list_reactions(uid, &path.space_name, path.board_id, path.issue_id)
30 .list_reactions(uid, &path.space_name, path.board_id, issue_id)
3031 .await?;
3132 Ok(AppResponseData::ok(list))
3233}
Modified lib/issues/src/handlers/reactions/remove_comment_reaction.rs View lib/issues/src/handlers/reactions/remove_comment_reaction.rs
@@ -25,12 +25,13 @@
2525 Query(q): Query<EmojiQuery>,
2626) -> Result<AppResponseData<NoData>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 service
2930 .remove_comment_reaction_by_emoji(
3031 uid,
3132 &path.space_name,
3233 path.board_id,
33 path.issue_id,
34 issue_id,
3435 path.comment_id,
3536 &q.emoji,
3637 )
Modified lib/issues/src/handlers/reactions/remove_reaction.rs View lib/issues/src/handlers/reactions/remove_reaction.rs
@@ -26,12 +26,13 @@
2626 Query(q): Query<EmojiQuery>,
2727) -> Result<AppResponseData<NoData>, ApiError> {
2828 let uid = require_uid(&session)?;
29 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2930 service
3031 .remove_reaction_by_emoji(
3132 uid,
3233 &path.space_name,
3334 path.board_id,
34 path.issue_id,
35 issue_id,
3536 &q.emoji,
3637 )
3738 .await?;
Modified lib/issues/src/handlers/repo_relations/create_repo_relation.rs View lib/issues/src/handlers/repo_relations/create_repo_relation.rs
@@ -29,8 +29,9 @@
2929 Json(cmd): Json<CreateRepoRelCmd>,
3030) -> Result<AppResponseData<RepoRelDto>, ApiError> {
3131 let uid = require_uid(&session)?;
32 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3233 let dto = service
33 .create_repo_relation(uid, &path.space_name, path.board_id, path.issue_id, cmd)
34 .create_repo_relation(uid, &path.space_name, path.board_id, issue_id, cmd)
3435 .await?;
3536 Ok(AppResponseData::ok(dto))
3637}
Modified lib/issues/src/handlers/repo_relations/delete_repo_relation.rs View lib/issues/src/handlers/repo_relations/delete_repo_relation.rs
@@ -24,12 +24,13 @@
2424 Path(path): Path<RepoRelPath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
2829 .delete_repo_relation(
2930 uid,
3031 &path.space_name,
3132 path.board_id,
32 path.issue_id,
33 issue_id,
3334 path.rel_id,
3435 )
3536 .await?;
Modified lib/issues/src/handlers/repo_relations/list_repo_relations.rs View lib/issues/src/handlers/repo_relations/list_repo_relations.rs
@@ -25,8 +25,9 @@
2525 Path(path): Path<IssuePath>,
2626) -> Result<AppResponseData<Vec<RepoRelDto>>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 let list = service
29 .list_repo_relations(uid, &path.space_name, path.board_id, path.issue_id)
30 .list_repo_relations(uid, &path.space_name, path.board_id, issue_id)
3031 .await?;
3132 Ok(AppResponseData::ok(list))
3233}
Modified lib/issues/src/handlers/stopwatch/get_stopwatch.rs View lib/issues/src/handlers/stopwatch/get_stopwatch.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<StopwatchPath>,
2525) -> Result<AppResponseData<StopwatchDto>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 let dto = service
28 .get_stopwatch(uid, &path.space_name, path.board_id, path.issue_id)
29 .get_stopwatch(uid, &path.space_name, path.board_id, issue_id)
2930 .await?;
3031 Ok(AppResponseData::ok(dto))
3132}
Modified lib/issues/src/handlers/stopwatch/start_stopwatch.rs View lib/issues/src/handlers/stopwatch/start_stopwatch.rs
@@ -25,8 +25,9 @@
2525 Path(path): Path<StopwatchPath>,
2626) -> Result<AppResponseData<StopwatchDto>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 let dto = service
29 .start_stopwatch(uid, &path.space_name, path.board_id, path.issue_id)
30 .start_stopwatch(uid, &path.space_name, path.board_id, issue_id)
3031 .await?;
3132 Ok(AppResponseData::ok(dto))
3233}
Modified lib/issues/src/handlers/stopwatch/stop_stopwatch.rs View lib/issues/src/handlers/stopwatch/stop_stopwatch.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<StopwatchPath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
28 .stop_stopwatch(uid, &path.space_name, path.board_id, path.issue_id)
29 .stop_stopwatch(uid, &path.space_name, path.board_id, issue_id)
2930 .await?;
3031 Ok(AppResponseData::ok(NoData))
3132}
Added lib/issues/src/handlers/tracked_time/create_tracked_time.rs View lib/issues/src/handlers/tracked_time/create_tracked_time.rs
@@ -0,0 +1,36 @@
1use axum::Json;
2use axum::extract::{Path, State};
3use export::rest::{AppResponseData, NoData};
4use session::Session;
5
6use crate::errors::ApiError;
7use crate::services::service::IssueService;
8use crate::services::tracked_time::{CreateTrackedTimeCmd, TrackedTimeDto, TrackedTimePath};
9use session::share::session::require_uid;
10
11#[utoipa::path(
12 post,
13 path = "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/times",
14 tag = "issues",
15 params(TrackedTimePath),
16 request_body = CreateTrackedTimeCmd,
17 responses(
18 (status = 200, description = "time logged", body = AppResponseData<TrackedTimeDto>),
19 (status = 400, description = "bad request", body = AppResponseData<NoData>)
20 )
21)]
22pub async fn create_tracked_time(
23 State(service): State<IssueService>,
24 session: Session,
25 Path(path): Path<TrackedTimePath>,
26 Json(cmd): Json<CreateTrackedTimeCmd>,
27) -> Result<AppResponseData<TrackedTimeDto>, ApiError> {
28 let uid = require_uid(&session)?;
29 let issue_id = service
30 .resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id)
31 .await?;
32 let dto = service
33 .create_tracked_time(uid, &path.space_name, path.board_id, issue_id, cmd)
34 .await?;
35 Ok(AppResponseData::ok(dto))
36}
Added lib/issues/src/handlers/tracked_time/delete_tracked_time.rs View lib/issues/src/handlers/tracked_time/delete_tracked_time.rs
@@ -0,0 +1,33 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4
5use crate::errors::ApiError;
6use crate::services::service::IssueService;
7use crate::services::tracked_time::TrackedTimeIdPath;
8use session::share::session::require_uid;
9
10#[utoipa::path(
11 delete,
12 path = "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/times/{time_id}",
13 tag = "issues",
14 params(TrackedTimeIdPath),
15 responses(
16 (status = 200, description = "time removed", body = AppResponseData<NoData>),
17 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
18 )
19)]
20pub async fn delete_tracked_time(
21 State(service): State<IssueService>,
22 session: Session,
23 Path(path): Path<TrackedTimeIdPath>,
24) -> Result<AppResponseData<NoData>, ApiError> {
25 let uid = require_uid(&session)?;
26 let issue_id = service
27 .resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id)
28 .await?;
29 service
30 .delete_tracked_time(uid, &path.space_name, path.board_id, issue_id, path.time_id)
31 .await?;
32 Ok(AppResponseData::empty())
33}
Modified lib/issues/src/handlers/tracked_time/list_tracked_time.rs View lib/issues/src/handlers/tracked_time/list_tracked_time.rs
@@ -25,8 +25,9 @@
2525 Query(q): Query<ListTrackedTimeQuery>,
2626) -> Result<AppResponseData<TrackedTimePage>, ApiError> {
2727 let uid = require_uid(&session)?;
28 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2829 let page = service
29 .list_tracked_time(uid, &path.space_name, path.board_id, path.issue_id, q)
30 .list_tracked_time(uid, &path.space_name, path.board_id, issue_id, q)
3031 .await?;
3132 Ok(AppResponseData::ok(page))
3233}
Modified lib/issues/src/handlers/tracked_time/mod.rs View lib/issues/src/handlers/tracked_time/mod.rs
@@ -1 +1,4 @@
11pub mod list_tracked_time;
2pub mod create_tracked_time;
3pub mod update_tracked_time;
4pub mod delete_tracked_time;
Added lib/issues/src/handlers/tracked_time/update_tracked_time.rs View lib/issues/src/handlers/tracked_time/update_tracked_time.rs
@@ -0,0 +1,43 @@
1use axum::Json;
2use axum::extract::{Path, State};
3use export::rest::{AppResponseData, NoData};
4use session::Session;
5
6use crate::errors::ApiError;
7use crate::services::service::IssueService;
8use crate::services::tracked_time::{TrackedTimeDto, TrackedTimeIdPath, UpdateTrackedTimeCmd};
9use session::share::session::require_uid;
10
11#[utoipa::path(
12 patch,
13 path = "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/times/{time_id}",
14 tag = "issues",
15 params(TrackedTimeIdPath),
16 request_body = UpdateTrackedTimeCmd,
17 responses(
18 (status = 200, description = "time updated", body = AppResponseData<TrackedTimeDto>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
20 )
21)]
22pub async fn update_tracked_time(
23 State(service): State<IssueService>,
24 session: Session,
25 Path(path): Path<TrackedTimeIdPath>,
26 Json(cmd): Json<UpdateTrackedTimeCmd>,
27) -> Result<AppResponseData<TrackedTimeDto>, ApiError> {
28 let uid = require_uid(&session)?;
29 let issue_id = service
30 .resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id)
31 .await?;
32 let dto = service
33 .update_tracked_time(
34 uid,
35 &path.space_name,
36 path.board_id,
37 issue_id,
38 path.time_id,
39 cmd,
40 )
41 .await?;
42 Ok(AppResponseData::ok(dto))
43}
Modified lib/issues/src/handlers/watches/delete_watch.rs View lib/issues/src/handlers/watches/delete_watch.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<WatchPath>,
2525) -> Result<AppResponseData<NoData>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 service
28 .delete_watch(uid, &path.space_name, path.board_id, path.issue_id)
29 .delete_watch(uid, &path.space_name, path.board_id, issue_id)
2930 .await?;
3031 Ok(AppResponseData::ok(NoData))
3132}
Modified lib/issues/src/handlers/watches/get_watch.rs View lib/issues/src/handlers/watches/get_watch.rs
@@ -24,8 +24,9 @@
2424 Path(path): Path<WatchPath>,
2525) -> Result<AppResponseData<WatchDto>, ApiError> {
2626 let uid = require_uid(&session)?;
27 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
2728 let dto = service
28 .get_watch(uid, &path.space_name, path.board_id, path.issue_id)
29 .get_watch(uid, &path.space_name, path.board_id, issue_id)
2930 .await?;
3031 Ok(AppResponseData::ok(dto))
3132}
Modified lib/issues/src/handlers/watches/upsert_watch.rs View lib/issues/src/handlers/watches/upsert_watch.rs
@@ -27,8 +27,9 @@
2727 Json(cmd): Json<UpsertWatchCmd>,
2828) -> Result<AppResponseData<WatchDto>, ApiError> {
2929 let uid = require_uid(&session)?;
30 let issue_id = service.resolve_issue_ref(uid, &path.space_name, path.board_id, &path.issue_id).await?;
3031 let dto = service
31 .upsert_watch(uid, &path.space_name, path.board_id, path.issue_id, cmd)
32 .upsert_watch(uid, &path.space_name, path.board_id, issue_id, cmd)
3233 .await?;
3334 Ok(AppResponseData::ok(dto))
3435}
Modified lib/issues/src/services/comments.rs View lib/issues/src/services/comments.rs
@@ -36,6 +36,12 @@
3636 pub total: i64,
3737}
3838
39#[derive(Debug, Clone, Serialize, ToSchema)]
40pub struct CommentTreeNode {
41 pub comment: CommentDto,
42 pub replies: Vec<CommentTreeNode>,
43}
44
3945#[derive(Debug, Clone, Deserialize, ToSchema)]
4046pub struct CreateCommentCmd {
4147 pub content: String,
@@ -59,7 +65,7 @@
5965pub struct CommentPath {
6066 pub space_name: String,
6167 pub board_id: Uuid,
62 pub issue_id: Uuid,
68 pub issue_id: String,
6369 pub comment_id: Uuid,
6470}
6571
@@ -345,6 +351,66 @@
345351 Ok(CommentPage { items, total })
346352 }
347353
354 pub async fn list_comments_tree(
355 &self,
356 caller: Uuid,
357 space_name: &str,
358 board_id: Uuid,
359 issue_id: Uuid,
360 ) -> Result<Vec<CommentTreeNode>, IssueError> {
361 let space = lookup_space_by_name(self, space_name).await?;
362 require_member(self, &space, caller).await?;
363 let board = lookup_board(self, &space, board_id, caller).await?;
364 let _ = lookup_issue(self, &board, issue_id).await?;
365
366 let rows = comment::Entity::find()
367 .filter(comment::Column::IssueId.eq(Some(issue_id)))
368 .order_by_asc(comment::Column::CreatedAt)
369 .all(self.db().read())
370 .await?;
371
372 let poster_ids: Vec<Uuid> = rows.iter().map(|c| c.poster_id).collect();
373 let user_map = self.load_user_briefs(&poster_ids).await?;
374
375 let dtos: Vec<CommentDto> = rows
376 .into_iter()
377 .map(|c| {
378 let poster = user_map.get(&c.poster_id).cloned();
379 CommentDto {
380 id: c.id,
381 issue_id: c.issue_id,
382 parent_id: c.parent_id,
383 poster,
384 content: c.content,
385 created_at: c.created_at,
386 updated_at: c.updated_at,
387 }
388 })
389 .collect();
390
391 let mut children: std::collections::HashMap<Option<Uuid>, Vec<CommentDto>> =
392 std::collections::HashMap::new();
393 for dto in dtos {
394 children.entry(dto.parent_id).or_default().push(dto);
395 }
396
397 fn build(
398 parent: Option<Uuid>,
399 children: &mut std::collections::HashMap<Option<Uuid>, Vec<CommentDto>>,
400 ) -> Vec<CommentTreeNode> {
401 let nodes = children.remove(&parent).unwrap_or_default();
402 nodes
403 .into_iter()
404 .map(|comment| CommentTreeNode {
405 comment: comment.clone(),
406 replies: build(Some(comment.id), children),
407 })
408 .collect()
409 }
410
411 Ok(build(None, &mut children))
412 }
413
348414 pub async fn get_comment(
349415 &self,
350416 caller: Uuid,
Modified lib/issues/src/services/content_history.rs View lib/issues/src/services/content_history.rs
@@ -39,7 +39,7 @@
3939pub struct ContentHistoryPath {
4040 pub space_name: String,
4141 pub board_id: Uuid,
42 pub issue_id: Uuid,
42 pub issue_id: String,
4343}
4444
4545impl IssueService {
Modified lib/issues/src/services/dependencies.rs View lib/issues/src/services/dependencies.rs
@@ -41,7 +41,7 @@
4141pub struct DependencyPath {
4242 pub space_name: String,
4343 pub board_id: Uuid,
44 pub issue_id: Uuid,
44 pub issue_id: String,
4545 pub dep_id: Uuid,
4646}
4747
@@ -118,6 +118,27 @@
118118 Ok(DependencyDto::from(inserted))
119119 }
120120
121 pub async fn list_dependents(
122 &self,
123 caller: Uuid,
124 space_name: &str,
125 board_id: Uuid,
126 issue_id: Uuid,
127 ) -> Result<Vec<DependencyDto>, IssueError> {
128 let space = lookup_space_by_name(self, space_name).await?;
129 require_member(self, &space, caller).await?;
130 let board = lookup_board(self, &space, board_id, caller).await?;
131 let _ = lookup_issue(self, &board, issue_id).await?;
132
133 let rows = issue_dependency::Entity::find()
134 .filter(issue_dependency::Column::DependencyId.eq(issue_id))
135 .order_by_asc(issue_dependency::Column::CreatedAt)
136 .all(self.db().read())
137 .await?;
138
139 Ok(rows.into_iter().map(DependencyDto::from).collect())
140 }
141
121142 pub async fn list_dependencies(
122143 &self,
123144 caller: Uuid,
Modified lib/issues/src/services/events.rs View lib/issues/src/services/events.rs
@@ -42,7 +42,7 @@
4242pub struct EventPath {
4343 pub space_name: String,
4444 pub board_id: Uuid,
45 pub issue_id: Uuid,
45 pub issue_id: String,
4646}
4747
4848impl IssueService {
Modified lib/issues/src/services/issue_users.rs View lib/issues/src/services/issue_users.rs
@@ -45,7 +45,7 @@
4545pub struct IssueUserPath {
4646 pub space_name: String,
4747 pub board_id: Uuid,
48 pub issue_id: Uuid,
48 pub issue_id: String,
4949 pub user_id: Uuid,
5050}
5151
Modified lib/issues/src/services/issues.rs View lib/issues/src/services/issues.rs
@@ -33,7 +33,7 @@
3333use crate::errors::IssueError;
3434use crate::services::service::IssueService;
3535use crate::share::authz::is_board_manager;
36use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member};
36use crate::share::lookup::{lookup_board, lookup_issue, lookup_issue_by_ref, lookup_space_by_name, require_member};
3737
3838#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
3939#[serde(rename_all = "snake_case")]
@@ -137,7 +137,8 @@
137137pub struct IssuePath {
138138 pub space_name: String,
139139 pub board_id: Uuid,
140 pub issue_id: Uuid,
140 /// issue uuid or board-scoped number (e.g. "123")
141 pub issue_id: String,
141142}
142143
143144pub async fn delete_issue_cascade_in_txn(
@@ -953,6 +954,89 @@
953954 self.build_issue_dto(updated).await
954955 }
955956
957}
958
959#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
960#[serde(rename_all = "snake_case")]
961pub enum BulkAction {
962 Close,
963 Reopen,
964}
965
966#[derive(Debug, Clone, Deserialize, ToSchema)]
967pub struct BulkIssuesCmd {
968 pub action: BulkAction,
969 pub issue_refs: Vec<String>,
970}
971
972#[derive(Debug, Clone, Serialize, ToSchema)]
973pub struct BulkResultDto {
974 pub updated: u64,
975}
976
977impl IssueService {
978 pub async fn bulk_update_state(
979 &self,
980 caller: Uuid,
981 space_name: &str,
982 board_id: Uuid,
983 cmd: BulkIssuesCmd,
984 ) -> Result<BulkResultDto, IssueError> {
985 if cmd.issue_refs.is_empty() || cmd.issue_refs.len() > 100 {
986 return Err(IssueError::BadRequest(
987 "issue_refs must contain 1..=100 entries".to_string(),
988 ));
989 }
990 let space = lookup_space_by_name(self, space_name).await?;
991 require_member(self, &space, caller).await?;
992 let board = lookup_board(self, &space, board_id, caller).await?;
993 let is_manager = is_board_manager(self, &space, caller).await?;
994
995 let mut updated = 0u64;
996 for issue_ref in cmd.issue_refs {
997 let Ok(item) = lookup_issue_by_ref(self, &board, &issue_ref).await else {
998 continue;
999 };
1000 let is_closed = item.closed_at.is_some();
1001 let should_close = matches!(cmd.action, BulkAction::Close);
1002 if is_closed == should_close {
1003 continue;
1004 }
1005 if item.author_id != caller && !is_manager {
1006 continue;
1007 }
1008 let now = OffsetDateTime::now_utc();
1009 issue_item::Entity::update_many()
1010 .col_expr(
1011 issue_item::Column::ClosedAt,
1012 sea_orm::sea_query::Expr::value(should_close.then_some(now)),
1013 )
1014 .col_expr(issue_item::Column::UpdatedAt, sea_orm::sea_query::Expr::value(now))
1015 .filter(issue_item::Column::Id.eq(item.id))
1016 .exec(self.db().write())
1017 .await?;
1018 self.write_event(
1019 self.db().write(),
1020 item.id,
1021 caller,
1022 if should_close {
1023 IssueEventType::Closed
1024 } else {
1025 IssueEventType::Reopened
1026 },
1027 None,
1028 None,
1029 None,
1030 None,
1031 )
1032 .await?;
1033 updated += 1;
1034 }
1035 Ok(BulkResultDto { updated })
1036 }
1037}
1038
1039impl IssueService {
9561040 pub async fn delete_issue(
9571041 &self,
9581042 caller: Uuid,
Modified lib/issues/src/services/labels.rs View lib/issues/src/services/labels.rs
@@ -99,7 +99,7 @@
9999pub struct IssueLabelPath {
100100 pub space_name: String,
101101 pub board_id: Uuid,
102 pub issue_id: Uuid,
102 pub issue_id: String,
103103 pub label_id: Uuid,
104104}
105105
Modified lib/issues/src/services/members.rs View lib/issues/src/services/members.rs
@@ -50,7 +50,7 @@
5050pub struct IssueMemberPath {
5151 pub space_name: String,
5252 pub board_id: Uuid,
53 pub issue_id: Uuid,
53 pub issue_id: String,
5454 pub member_id: Uuid,
5555}
5656
Modified lib/issues/src/services/pins.rs View lib/issues/src/services/pins.rs
@@ -44,6 +44,38 @@
4444 pub pin_id: Uuid,
4545}
4646
47#[derive(Debug, Clone, Deserialize, ToSchema)]
48pub struct UpdatePinCmd {
49 pub pin_order: i32,
50}
51
52impl IssueService {
53 pub async fn update_pin(
54 &self,
55 caller: Uuid,
56 space_name: &str,
57 board_id: Uuid,
58 pin_id: Uuid,
59 cmd: UpdatePinCmd,
60 ) -> Result<PinDto, IssueError> {
61 let space = lookup_space_by_name(self, space_name).await?;
62 require_member(self, &space, caller).await?;
63 require_board_manage(self, &space, caller).await?;
64 let row = issue_pin::Entity::find_by_id(pin_id)
65 .filter(issue_pin::Column::CollectionId.eq(board_id))
66 .one(self.db().write())
67 .await?
68 .ok_or(IssueError::NotFound)?;
69 if cmd.pin_order < 0 {
70 return Err(IssueError::BadRequest("pin_order must be >= 0".to_string()));
71 }
72 let mut am: issue_pin::ActiveModel = row.into();
73 am.pin_order = Set(cmd.pin_order);
74 let updated = am.update(self.db().write()).await?;
75 Ok(PinDto::from(updated))
76 }
77}
78
4779impl IssueService {
4880 pub async fn create_pin(
4981 &self,
Modified lib/issues/src/services/reactions.rs View lib/issues/src/services/reactions.rs
@@ -48,7 +48,7 @@
4848pub struct ReactionPath {
4949 pub space_name: String,
5050 pub board_id: Uuid,
51 pub issue_id: Uuid,
51 pub issue_id: String,
5252 pub reaction_id: Uuid,
5353}
5454
@@ -61,7 +61,7 @@
6161pub struct CommentReactionPath {
6262 pub space_name: String,
6363 pub board_id: Uuid,
64 pub issue_id: Uuid,
64 pub issue_id: String,
6565 pub comment_id: Uuid,
6666}
6767
Modified lib/issues/src/services/repo_relations.rs View lib/issues/src/services/repo_relations.rs
@@ -39,7 +39,7 @@
3939pub struct RepoRelPath {
4040 pub space_name: String,
4141 pub board_id: Uuid,
42 pub issue_id: Uuid,
42 pub issue_id: String,
4343 pub rel_id: Uuid,
4444}
4545
Modified lib/issues/src/services/service.rs View lib/issues/src/services/service.rs
@@ -66,6 +66,21 @@
6666 }
6767
6868 #[allow(clippy::too_many_arguments)]
69 pub async fn resolve_issue_ref(
70 &self,
71 caller: Uuid,
72 space_name: &str,
73 board_id: Uuid,
74 issue_ref: &str,
75 ) -> Result<Uuid, IssueError> {
76 let space = crate::share::lookup::lookup_space_by_name(self, space_name).await?;
77 crate::share::lookup::require_member(self, &space, caller).await?;
78 let board = crate::share::lookup::lookup_board(self, &space, board_id, caller).await?;
79 Ok(crate::share::lookup::lookup_issue_by_ref(self, &board, issue_ref)
80 .await?
81 .id)
82 }
83
6984 pub(crate) async fn write_event(
7085 &self,
7186 txn: &impl ConnectionTrait,
Modified lib/issues/src/services/stopwatch.rs View lib/issues/src/services/stopwatch.rs
@@ -13,18 +13,23 @@
1313
1414#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
1515pub struct StopwatchDto {
16 pub id: Uuid,
16 pub running: bool,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub id: Option<Uuid>,
1719 pub issue_id: Uuid,
20 #[serde(skip_serializing_if = "Option::is_none")]
1821 pub user: Option<UserBriefDto>,
19 pub started_at: OffsetDateTime,
20 pub created_at: OffsetDateTime,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub started_at: Option<OffsetDateTime>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub created_at: Option<OffsetDateTime>,
2126}
2227
2328#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
2429pub struct StopwatchPath {
2530 pub space_name: String,
2631 pub board_id: Uuid,
27 pub issue_id: Uuid,
32 pub issue_id: String,
2833}
2934
3035impl IssueService {
@@ -44,18 +49,29 @@
4449 .filter(stopwatch::Column::IssueId.eq(issue_id))
4550 .filter(stopwatch::Column::UserId.eq(caller))
4651 .one(self.db().read())
47 .await?
48 .ok_or(IssueError::NotFound)?;
52 .await?;
4953
54 let Some(sw) = sw else {
55 return Ok(StopwatchDto {
56 running: false,
57 id: None,
58 issue_id,
59 user: None,
60 started_at: None,
61 created_at: None,
62 });
63 };
64
5065 let user_map = self.load_user_briefs(&[sw.user_id]).await?;
5166 let user = user_map.get(&sw.user_id).cloned();
5267
5368 Ok(StopwatchDto {
54 id: sw.id,
69 running: true,
70 id: Some(sw.id),
5571 issue_id: sw.issue_id,
5672 user,
57 started_at: sw.started_at,
58 created_at: sw.created_at,
73 started_at: Some(sw.started_at),
74 created_at: Some(sw.created_at),
5975 })
6076 }
6177
@@ -97,11 +113,12 @@
97113 let user = user_map.get(&caller).cloned();
98114
99115 Ok(StopwatchDto {
100 id: inserted.id,
116 running: true,
117 id: Some(inserted.id),
101118 issue_id: inserted.issue_id,
102119 user,
103 started_at: inserted.started_at,
104 created_at: inserted.created_at,
120 started_at: Some(inserted.started_at),
121 created_at: Some(inserted.created_at),
105122 })
106123 }
107124
Modified lib/issues/src/services/tracked_time.rs View lib/issues/src/services/tracked_time.rs
@@ -1,5 +1,5 @@
11use models::issue::tracked_time;
2use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
2use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set};
33use serde::{Deserialize, Serialize};
44use space::services::spaces::UserBriefDto;
55use time::OffsetDateTime;
@@ -36,10 +36,125 @@
3636pub struct TrackedTimePath {
3737 pub space_name: String,
3838 pub board_id: Uuid,
39 pub issue_id: Uuid,
39 pub issue_id: String,
40}
41
42#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
43pub struct TrackedTimeIdPath {
44 pub space_name: String,
45 pub board_id: Uuid,
46 pub issue_id: String,
47 pub time_id: Uuid,
48}
49
50#[derive(Debug, Clone, Deserialize, ToSchema)]
51pub struct CreateTrackedTimeCmd {
52 pub seconds: i64,
53}
54
55#[derive(Debug, Clone, Deserialize, ToSchema)]
56pub struct UpdateTrackedTimeCmd {
57 pub seconds: i64,
4058}
4159
4260impl IssueService {
61 pub async fn create_tracked_time(
62 &self,
63 caller: Uuid,
64 space_name: &str,
65 board_id: Uuid,
66 issue_id: Uuid,
67 cmd: CreateTrackedTimeCmd,
68 ) -> Result<TrackedTimeDto, IssueError> {
69 if cmd.seconds <= 0 {
70 return Err(IssueError::BadRequest("seconds must be positive".to_string()));
71 }
72 let space = lookup_space_by_name(self, space_name).await?;
73 require_member(self, &space, caller).await?;
74 let board = lookup_board(self, &space, board_id, caller).await?;
75 let _ = lookup_issue(self, &board, issue_id).await?;
76 let row = tracked_time::ActiveModel {
77 id: Set(Uuid::now_v7()),
78 issue_id: Set(issue_id),
79 user_id: Set(caller),
80 seconds: Set(cmd.seconds),
81 created_at: Set(OffsetDateTime::now_utc()),
82 }
83 .insert(self.db().write())
84 .await?;
85 let user_map = self.load_user_briefs(&[row.user_id]).await?;
86 Ok(TrackedTimeDto {
87 id: row.id,
88 issue_id: row.issue_id,
89 user: user_map.get(&row.user_id).cloned(),
90 seconds: row.seconds,
91 created_at: row.created_at,
92 })
93 }
94
95 pub async fn update_tracked_time(
96 &self,
97 caller: Uuid,
98 space_name: &str,
99 board_id: Uuid,
100 issue_id: Uuid,
101 time_id: Uuid,
102 cmd: UpdateTrackedTimeCmd,
103 ) -> Result<TrackedTimeDto, IssueError> {
104 if cmd.seconds <= 0 {
105 return Err(IssueError::BadRequest("seconds must be positive".to_string()));
106 }
107 let space = lookup_space_by_name(self, space_name).await?;
108 require_member(self, &space, caller).await?;
109 let board = lookup_board(self, &space, board_id, caller).await?;
110 let _ = lookup_issue(self, &board, issue_id).await?;
111 let row = tracked_time::Entity::find_by_id(time_id)
112 .filter(tracked_time::Column::IssueId.eq(issue_id))
113 .one(self.db().write())
114 .await?
115 .ok_or(IssueError::NotFound)?;
116 if row.user_id != caller {
117 return Err(IssueError::Forbidden);
118 }
119 let mut am: tracked_time::ActiveModel = row.into();
120 am.seconds = Set(cmd.seconds);
121 let updated = am.update(self.db().write()).await?;
122 let user_map = self.load_user_briefs(&[updated.user_id]).await?;
123 Ok(TrackedTimeDto {
124 id: updated.id,
125 issue_id: updated.issue_id,
126 user: user_map.get(&updated.user_id).cloned(),
127 seconds: updated.seconds,
128 created_at: updated.created_at,
129 })
130 }
131
132 pub async fn delete_tracked_time(
133 &self,
134 caller: Uuid,
135 space_name: &str,
136 board_id: Uuid,
137 issue_id: Uuid,
138 time_id: Uuid,
139 ) -> Result<(), IssueError> {
140 let space = lookup_space_by_name(self, space_name).await?;
141 require_member(self, &space, caller).await?;
142 let board = lookup_board(self, &space, board_id, caller).await?;
143 let _ = lookup_issue(self, &board, issue_id).await?;
144 let row = tracked_time::Entity::find_by_id(time_id)
145 .filter(tracked_time::Column::IssueId.eq(issue_id))
146 .one(self.db().read())
147 .await?
148 .ok_or(IssueError::NotFound)?;
149 if row.user_id != caller {
150 return Err(IssueError::Forbidden);
151 }
152 tracked_time::Entity::delete_by_id(time_id)
153 .exec(self.db().write())
154 .await?;
155 Ok(())
156 }
157
43158 pub async fn list_tracked_time(
44159 &self,
45160 caller: Uuid,
Modified lib/issues/src/services/watches.rs View lib/issues/src/services/watches.rs
@@ -30,7 +30,7 @@
3030pub struct WatchPath {
3131 pub space_name: String,
3232 pub board_id: Uuid,
33 pub issue_id: Uuid,
33 pub issue_id: String,
3434}
3535
3636impl IssueService {
Modified lib/issues/src/share/lookup.rs View lib/issues/src/share/lookup.rs
@@ -72,6 +72,26 @@
7272 .ok_or(IssueError::NotFound)
7373}
7474
75pub async fn lookup_issue_by_ref(
76 service: &IssueService,
77 board: &issues::Model,
78 issue_ref: &str,
79) -> Result<issue_item::Model, IssueError> {
80 let base = issue_item::Entity::find().filter(issue_item::Column::CollectionId.eq(board.id));
81 let item = if let Ok(id) = Uuid::parse_str(issue_ref) {
82 base.filter(issue_item::Column::Id.eq(id))
83 .one(service.db().read())
84 .await?
85 } else if let Ok(number) = issue_ref.parse::<i32>() {
86 base.filter(issue_item::Column::AutoincrementId.eq(number))
87 .one(service.db().read())
88 .await?
89 } else {
90 None
91 };
92 item.ok_or(IssueError::NotFound)
93}
94
7595pub async fn require_member(
7696 service: &IssueService,
7797 space: &spaces::Model,
Modified lib/issues/src/openapi.rs View lib/issues/src/openapi.rs
@@ -15,6 +15,10 @@
1515 crate::handlers::issues::update_issue::update_issue,
1616 crate::handlers::issues::delete_issue::delete_issue,
1717 crate::handlers::issues::close_issue::close_issue,
18 crate::handlers::issues::bulk_issues::bulk_issues,
19 crate::handlers::dependencies::list_dependents::list_dependents,
20 crate::handlers::pins::update_pin::update_pin,
21 crate::handlers::comments::list_comments_tree::list_comments_tree,
1822 crate::handlers::issues::reopen_issue::reopen_issue,
1923 crate::handlers::comments::list_comments::list_comments,
2024 crate::handlers::comments::create_comment::create_comment,
@@ -58,6 +62,9 @@
5862 crate::handlers::stopwatch::start_stopwatch::start_stopwatch,
5963 crate::handlers::stopwatch::stop_stopwatch::stop_stopwatch,
6064 crate::handlers::tracked_time::list_tracked_time::list_tracked_time,
65 crate::handlers::tracked_time::create_tracked_time::create_tracked_time,
66 crate::handlers::tracked_time::update_tracked_time::update_tracked_time,
67 crate::handlers::tracked_time::delete_tracked_time::delete_tracked_time,
6168 crate::handlers::issue_users::list_issue_users::list_issue_users,
6269 crate::handlers::issue_users::update_issue_user::update_issue_user,
6370 crate::handlers::repo_relations::list_repo_relations::list_repo_relations,
@@ -149,6 +156,7 @@
149156 models::issue::issue_member::IssueMemberKind,
150157 crate::services::pins::PinDto,
151158 crate::services::pins::CreatePinCmd,
159 crate::services::pins::UpdatePinCmd,
152160 crate::services::pins::PinPath,
153161 crate::services::events::EventDto,
154162 crate::services::events::EventPage,
@@ -166,6 +174,14 @@
166174 crate::services::tracked_time::TrackedTimeDto,
167175 crate::services::tracked_time::TrackedTimePage,
168176 crate::services::tracked_time::TrackedTimePath,
177 crate::services::tracked_time::TrackedTimeIdPath,
178 crate::services::tracked_time::CreateTrackedTimeCmd,
179 crate::services::tracked_time::UpdateTrackedTimeCmd,
180 crate::services::issues::BulkAction,
181 crate::services::issues::BulkIssuesCmd,
182 crate::services::issues::BulkResultDto,
183 crate::services::comments::CommentTreeNode,
184 crate::services::tracked_time::TrackedTimePath,
169185 crate::services::tracked_time::ListTrackedTimeQuery,
170186 crate::services::issue_users::IssueUserDto,
171187 crate::services::issue_users::IssueUserPage,
Modified lib/issues/src/routes.rs View lib/issues/src/routes.rs
@@ -75,6 +75,10 @@
7575 get(list_issues).post(create_issue),
7676 )
7777 .route(
78 "/issues/{space_name}/boards/{board_id}/issues/bulk",
79 post(crate::handlers::issues::bulk_issues::bulk_issues),
80 )
81 .route(
7882 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}",
7983 get(get_issue).patch(update_issue).delete(delete_issue),
8084 )
@@ -91,6 +95,10 @@
9195 get(list_comments).post(create_comment),
9296 )
9397 .route(
98 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/comments/tree",
99 get(crate::handlers::comments::list_comments_tree::list_comments_tree),
100 )
101 .route(
94102 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/comments/{comment_id}",
95103 get(get_comment)
96104 .patch(update_comment)
@@ -156,7 +164,7 @@
156164 )
157165 .route(
158166 "/issues/{space_name}/boards/{board_id}/pins/{pin_id}",
159 delete(delete_pin),
167 delete(delete_pin).patch(crate::handlers::pins::update_pin::update_pin),
160168 )
161169 .route(
162170 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/events",
@@ -167,6 +175,10 @@
167175 get(list_dependencies).post(create_dependency),
168176 )
169177 .route(
178 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/dependents",
179 get(crate::handlers::dependencies::list_dependents::list_dependents),
180 )
181 .route(
170182 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/dependencies/{dep_id}",
171183 delete(delete_dependency),
172184 )
@@ -188,7 +200,15 @@
188200 )
189201 .route(
190202 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/times",
191 get(list_tracked_time),
203 get(list_tracked_time)
204 .post(crate::handlers::tracked_time::create_tracked_time::create_tracked_time),
205 )
206 .route(
207 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/times/{time_id}",
208 axum::routing::patch(
209 crate::handlers::tracked_time::update_tracked_time::update_tracked_time,
210 )
211 .delete(crate::handlers::tracked_time::delete_tracked_time::delete_tracked_time),
192212 )
193213 .route(
194214 "/issues/{space_name}/boards/{board_id}/issues/{issue_id}/users",