diff --git a/apps/backend/crates/handler/src/handlers/my_tasks.rs b/apps/backend/crates/handler/src/handlers/my_tasks.rs index 3edf96aba..ce0c17af9 100644 --- a/apps/backend/crates/handler/src/handlers/my_tasks.rs +++ b/apps/backend/crates/handler/src/handlers/my_tasks.rs @@ -1,14 +1,12 @@ use axum::{ Json, extract::{Path, Query, State}, - http::StatusCode, }; -use axum_valid::Valid; use chrono::{Duration, NaiveDate, Utc}; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, Condition, ConnectionTrait, EntityTrait, JoinType, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait, - TransactionTrait, prelude::Uuid, sea_query::LockType, + TransactionTrait, prelude::Uuid, }; use crate::AppState; @@ -17,52 +15,16 @@ use crate::extractors::AuthUser; use crate::openapi::CrudErrors; use entity::{ drive_folders, project_members, project_statuses, project_task_counters, projects, - scopes::Scope, task_assignees, tasks, users, + scopes::Scope, tasks, users, }; use payload::my_tasks::*; use payload::projects::ProjectResponse; -use payload::tasks::TaskResponse; use service::db::is_postgres_unique_violation; -use service::task_activities::record_activity; fn personal_project_key(user_id: Uuid) -> String { let id_hex = user_id.simple().to_string().to_ascii_uppercase(); format!("ME{}", &id_hex[..4]) } -async fn next_seq_id(db: &sea_orm::DatabaseTransaction, project_id: Uuid) -> Result { - let existing = project_task_counters::Entity::find_by_id(project_id) - .lock(LockType::Update) - .one(db) - .await?; - Ok(match existing { - Some(c) => { - let new_seq = c.last_seq + 1; - let mut active: project_task_counters::ActiveModel = c.into(); - active.last_seq = Set(new_seq); - active.update(db).await?.last_seq - } - None => { - project_task_counters::ActiveModel { - project_id: Set(project_id), - last_seq: Set(1), - } - .insert(db) - .await? - .last_seq - } - }) -} - -async fn default_status_id(db: &C, project_id: Uuid) -> Result { - project_statuses::Entity::find() - .filter(project_statuses::Column::ProjectId.eq(project_id)) - .filter(project_statuses::Column::IsDefault.eq(true)) - .one(db) - .await? - .map(|s| s.id) - .ok_or(AppError::NotFound) -} - async fn seed_personal_project_defaults( db: &C, project_id: Uuid, @@ -419,83 +381,3 @@ pub async fn list_my_tasks( total, })) } - -#[axum::debug_handler] -#[utoipa::path( - post, - path = "/tasks", - tag = "My Tasks", - summary = "クイックキャプチャ(個人プロジェクトへタスク作成)", - params(("tenant_id" = Uuid, Path, description = "テナントID")), - request_body = QuickCaptureRequest, - responses( - (status = 201, description = "作成されたタスク", body = TaskResponse), - CrudErrors, - ) -)] -pub async fn create_my_task( - State(state): State, - auth: AuthUser, - Path(tenant_id): Path, - Valid(Json(payload)): Valid>, -) -> Result<(StatusCode, Json), AppError> { - auth.require_scope(Scope::WriteTask)?; - auth.ensure_tenant_access(&state, tenant_id, None).await?; - - let personal = get_or_create_personal_project(&state, tenant_id, auth.user_id).await?; - let status_id = default_status_id(&state.db, personal.id).await?; - - let txn = state.db.begin().await?; - let seq_id = next_seq_id(&txn, personal.id).await?; - let priority = payload.priority.unwrap_or(tasks::TaskPriority::Medium); - - let model = tasks::ActiveModel { - id: Set(Uuid::new_v4()), - project_id: Set(personal.id), - seq_id: Set(seq_id), - title: Set(payload.title), - description: Set(payload.note), - status_id: Set(status_id), - priority: Set(priority), - progress_pct: Set(0), - parent_task_id: Set(None), - milestone_id: Set(None), - sprint_id: Set(None), - soft_deadline: Set(payload.soft_deadline.map(Into::into)), - hard_deadline: Set(None), - estimated_minutes: Set(None), - is_archived: Set(false), - created_by: Set(auth.user_id), - created_at: Set(Utc::now().into()), - updated_at: Set(Utc::now().into()), - completed_at: Set(None), - deleted_at: Set(None), - } - .insert(&txn) - .await?; - - task_assignees::ActiveModel { - id: Set(Uuid::new_v4()), - task_id: Set(model.id), - user_id: Set(auth.user_id), - role: Set("assignee".into()), - assigned_at: Set(Utc::now().into()), - } - .insert(&txn) - .await?; - - record_activity( - &txn, - model.id, - Some(auth.user_id), - "task_created", - serde_json::json!({}), - ) - .await?; - - txn.commit().await?; - Ok(( - StatusCode::CREATED, - Json(service::task_responses::build_task_response(&state.db, model).await?), - )) -} diff --git a/apps/backend/crates/handler/src/routes/tenants.rs b/apps/backend/crates/handler/src/routes/tenants.rs index 4e912c6da..59f47a2d8 100644 --- a/apps/backend/crates/handler/src/routes/tenants.rs +++ b/apps/backend/crates/handler/src/routes/tenants.rs @@ -141,7 +141,6 @@ pub fn routes() -> OpenApiRouter { "/{tenant_id}/users/me", OpenApiRouter::::new() .routes(routes!(crate::handlers::my_tasks::get_personal_project)) - .routes(routes!(crate::handlers::my_tasks::list_my_tasks)) - .routes(routes!(crate::handlers::my_tasks::create_my_task)), + .routes(routes!(crate::handlers::my_tasks::list_my_tasks)), ) } diff --git a/apps/backend/crates/payload/src/my_tasks.rs b/apps/backend/crates/payload/src/my_tasks.rs index 2f21505fc..a799f4f0f 100644 --- a/apps/backend/crates/payload/src/my_tasks.rs +++ b/apps/backend/crates/payload/src/my_tasks.rs @@ -1,7 +1,6 @@ use sea_orm::prelude::Uuid; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use validator::Validate; use entity::tasks; @@ -31,15 +30,6 @@ fn default_limit() -> u64 { 50 } -#[derive(Validate, Deserialize, ToSchema)] -pub struct QuickCaptureRequest { - #[validate(length(min = 1, max = 255))] - pub title: String, - pub soft_deadline: Option>, - pub priority: Option, - pub note: Option, -} - #[derive(Serialize, ToSchema)] pub struct MyTaskProjectInfo { #[schema(value_type = String, format = "uuid")] diff --git a/apps/backend/tests/my_tasks_integration.rs b/apps/backend/tests/my_tasks_integration.rs index 6a00ee4d0..c369499a0 100644 --- a/apps/backend/tests/my_tasks_integration.rs +++ b/apps/backend/tests/my_tasks_integration.rs @@ -68,8 +68,10 @@ async fn personal_project_is_idempotent() { assert_eq!(first, second); } +/// quick-capture API 撤去の回帰テスト(#363)。 +/// 撤去前は 201 CREATED を返していたため、撤去前のコードでは fail する。 #[tokio::test] -async fn quick_capture_and_list() { +async fn quick_capture_endpoint_is_removed() { let mut app = TestApp::new().await; let (_user, tp) = setup(&mut app).await; let base = my_tasks_base(tp.tenant_id); @@ -80,21 +82,55 @@ async fn quick_capture_and_list() { ) .await .status(), + StatusCode::METHOD_NOT_ALLOWED + ); +} + +#[tokio::test] +async fn list_returns_only_assigned_tasks() { + let mut app = TestApp::new().await; + let (user, tp) = setup(&mut app).await; + let status_id = create_status(&app, &tp, "Todo", false).await; + let path = format!( + "/v1/tenants/{}/projects/{}/tasks", + tp.tenant_id, tp.project_id + ); + let assignee = serde_json::json!([{"user_id": user.id, "role": "assignee"}]); + // 成功系: 自分に割り当てられたタスクは一覧に載る + assert_eq!( + app.post_json_with_session( + &path, + serde_json::json!({"title": "Assigned", "status_id": status_id, "assignees": assignee}), + ) + .await + .status(), + StatusCode::CREATED + ); + // 対照: 未割り当てのタスクは載らない + assert_eq!( + app.post_json_with_session( + &path, + serde_json::json!({"title": "Unassigned", "status_id": status_id}), + ) + .await + .status(), StatusCode::CREATED ); let body: serde_json::Value = app - .get_with_session(&format!("{base}/tasks?filter=all")) + .get_with_session(&format!("{}/tasks?filter=all", my_tasks_base(tp.tenant_id))) .await .json() .await .unwrap(); - assert!( - body["tasks"] - .as_array() - .unwrap() - .iter() - .any(|t| t["title"] == "Buy milk") - ); + let titles: Vec<&str> = body["tasks"] + .as_array() + .unwrap() + .iter() + .filter_map(|t| t["title"].as_str()) + .collect(); + assert!(titles.contains(&"Assigned")); + assert!(!titles.contains(&"Unassigned")); + assert_eq!(body["total"], 1); } #[tokio::test] diff --git a/apps/cli/src/api/paths.ts b/apps/cli/src/api/paths.ts index 6ebde5d69..e3a2658d5 100644 --- a/apps/cli/src/api/paths.ts +++ b/apps/cli/src/api/paths.ts @@ -468,21 +468,5 @@ export interface ApiPaths { 200: { content: { "application/json": MyTasksListResponse } }; }; }; - post: { - parameters: { path: { tenant_id: string } }; - requestBody: { - content: { - "application/json": { - title: string; - soft_deadline?: string; - priority?: TaskPriority; - note?: string; - }; - }; - }; - responses: { - 201: { content: { "application/json": Task } }; - }; - }; }; } diff --git a/apps/cli/src/commands/__tests__/commands.test.ts b/apps/cli/src/commands/__tests__/commands.test.ts index 6d391d352..6c0cbfbf0 100644 --- a/apps/cli/src/commands/__tests__/commands.test.ts +++ b/apps/cli/src/commands/__tests__/commands.test.ts @@ -81,13 +81,15 @@ describe("command registration and primary branches", () => { expect(mocks.saveConfigFile).toHaveBeenCalledWith({ tenant_id: "tenant-2" }); }); - it("my parses add and sends its title and priority", async () => { + it("my parses list and sends its filter", async () => { await programWith(registerMyCommands).parseAsync([ - "node", "task", "my", "add", "Golden task", "--priority", "high", + "node", "task", "my", "list", "--filter", "today", ]); - expect(mocks.POST).toHaveBeenCalledWith( + expect(mocks.GET).toHaveBeenCalledWith( "/v1/tenants/{tenant_id}/users/me/tasks", - expect.objectContaining({ body: { title: "Golden task", priority: "high" } }), + expect.objectContaining({ + params: expect.objectContaining({ query: { filter: "today" } }), + }), ); }); diff --git a/apps/cli/src/commands/my.ts b/apps/cli/src/commands/my.ts index 48c25acb4..681053b9a 100644 --- a/apps/cli/src/commands/my.ts +++ b/apps/cli/src/commands/my.ts @@ -1,6 +1,5 @@ import { Command } from "commander"; import { getClient, getTenantId } from "../api/client"; -import type { TaskPriority } from "../api/paths"; import { getOutputOptions } from "../utils/command"; import type { OutputOptions } from "../utils/output"; import { print } from "../utils/output"; @@ -10,7 +9,6 @@ import { findDoneStatusId } from "../utils/statuses"; type MyCommandOptions = OutputOptions & { filter?: string; - priority?: string; }; export function registerMyCommands(program: Command): void { @@ -37,29 +35,6 @@ export function registerMyCommands(program: Command): void { print(unwrapApiResult(result), output); }); - my - .command("add") - .description("Quick-capture a task to personal inbox") - .argument("", "Task title") - .option("--priority <priority>", "Task priority") - .action(async (title: string, opts: MyCommandOptions, cmd) => { - const output = getOutputOptions(cmd); - const client = getClient(); - const tenantId = getTenantId(); - const body: { - title: string; - priority?: TaskPriority; - } = { title }; - if (opts.priority) { - body.priority = opts.priority as TaskPriority; - } - const result = await client.POST("/v1/tenants/{tenant_id}/users/me/tasks", { - params: { path: { tenant_id: tenantId } }, - body, - }); - print(unwrapApiResult(result), output); - }); - my .command("complete") .description("Complete a personal or assigned task by ref (e.g. ME-3)") diff --git a/apps/frontend/openapi.json b/apps/frontend/openapi.json index 842a73a46..e35262981 100644 --- a/apps/frontend/openapi.json +++ b/apps/frontend/openapi.json @@ -17053,117 +17053,6 @@ } } } - }, - "post": { - "tags": ["My Tasks"], - "summary": "クイックキャプチャ(個人プロジェクトへタスク作成)", - "operationId": "create_my_task", - "parameters": [ - { - "name": "tenant_id", - "in": "path", - "description": "テナントID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuickCaptureRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "作成されたタスク", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TaskResponse" - } - } - } - }, - "401": { - "description": "ログインまたはセッションが必要です", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "API 共通のエラー応答ボディ。", - "required": ["message"], - "properties": { - "message": { - "type": "string", - "example": "internal-error" - } - } - } - } - } - }, - "403": { - "description": "この操作は許可されていません", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "API 共通のエラー応答ボディ。", - "required": ["message"], - "properties": { - "message": { - "type": "string", - "example": "internal-error" - } - } - } - } - } - }, - "404": { - "description": "リソースが見つかりません", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "API 共通のエラー応答ボディ。", - "required": ["message"], - "properties": { - "message": { - "type": "string", - "example": "internal-error" - } - } - } - } - } - }, - "500": { - "description": "サーバー側で問題が発生しました。時間をおいて再度お試しください", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "API 共通のエラー応答ボディ。", - "required": ["message"], - "properties": { - "message": { - "type": "string", - "example": "internal-error" - } - } - } - } - } - } - } } }, "/v1/users/me/notification-settings/{project_id}": { @@ -19461,32 +19350,6 @@ } } }, - "QuickCaptureRequest": { - "type": "object", - "required": ["title"], - "properties": { - "note": { - "type": ["string", "null"] - }, - "priority": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/TaskPriority" - } - ] - }, - "soft_deadline": { - "type": ["string", "null"], - "format": "date-time" - }, - "title": { - "type": "string" - } - } - }, "RegisterRequest": { "type": "object", "required": ["username", "email", "password"],