From 284e33511a487c76a86550ce0903c6db88be09fa Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 16 Jun 2025 21:25:19 -0400 Subject: [PATCH] Distinguish in progress --- backend/src/bin/generate_types.rs | 3 + backend/src/models/task.rs | 58 +++++++++++++++++++ backend/src/routes/tasks.rs | 6 +- frontend/src/components/tasks/TaskCard.tsx | 18 +++--- .../src/components/tasks/TaskKanbanBoard.tsx | 12 +--- frontend/src/pages/project-tasks.tsx | 12 +--- shared/types.ts | 2 + 7 files changed, 77 insertions(+), 34 deletions(-) diff --git a/backend/src/bin/generate_types.rs b/backend/src/bin/generate_types.rs index 32a6a652..e70d2875 100644 --- a/backend/src/bin/generate_types.rs +++ b/backend/src/bin/generate_types.rs @@ -71,6 +71,8 @@ export {} export {} +export {} + export {}"#, bloop_backend::models::ApiResponse::<()>::decl(), bloop_backend::executor::ExecutorConfig::decl(), @@ -80,6 +82,7 @@ export {}"#, bloop_backend::models::task::CreateTask::decl(), bloop_backend::models::task::TaskStatus::decl(), bloop_backend::models::task::Task::decl(), + bloop_backend::models::task::TaskWithAttemptStatus::decl(), bloop_backend::models::task::UpdateTask::decl(), bloop_backend::models::task_attempt::TaskAttemptStatus::decl(), bloop_backend::models::task_attempt::TaskAttempt::decl(), diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index ebab2bb3..82eec894 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -28,6 +28,19 @@ pub struct Task { pub updated_at: DateTime, } +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct TaskWithAttemptStatus { + pub id: Uuid, + pub project_id: Uuid, + pub title: String, + pub description: Option, + pub status: TaskStatus, + pub created_at: DateTime, + pub updated_at: DateTime, + pub has_in_progress_attempt: bool, +} + #[derive(Debug, Deserialize, TS)] #[ts(export)] pub struct CreateTask { @@ -58,6 +71,51 @@ impl Task { .await } + pub async fn find_by_project_id_with_attempt_status(pool: &PgPool, project_id: Uuid) -> Result, sqlx::Error> { + let records = sqlx::query!( + r#"SELECT + t.id, + t.project_id, + t.title, + t.description, + t.status as "status!: TaskStatus", + t.created_at, + t.updated_at, + CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_in_progress_attempt!" + FROM tasks t + LEFT JOIN ( + SELECT DISTINCT ta.task_id + FROM task_attempts ta + INNER JOIN ( + SELECT task_attempt_id, MAX(created_at) as latest_created_at + FROM task_attempt_activities + GROUP BY task_attempt_id + ) latest_activity ON ta.id = latest_activity.task_attempt_id + INNER JOIN task_attempt_activities taa ON ta.id = taa.task_attempt_id + AND taa.created_at = latest_activity.latest_created_at + WHERE taa.status = 'inprogress' + ) in_progress_attempts ON t.id = in_progress_attempts.task_id + WHERE t.project_id = $1 + ORDER BY t.created_at DESC"#, + project_id + ) + .fetch_all(pool) + .await?; + + let tasks = records.into_iter().map(|record| TaskWithAttemptStatus { + id: record.id, + project_id: record.project_id, + title: record.title, + description: record.description, + status: record.status, + created_at: record.created_at, + updated_at: record.updated_at, + has_in_progress_attempt: record.has_in_progress_attempt, + }).collect(); + + Ok(tasks) + } + pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { sqlx::query_as!( Task, diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 0f8c75a0..756b1764 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use crate::models::{ ApiResponse, project::Project, - task::{Task, CreateTask, UpdateTask}, + task::{Task, CreateTask, UpdateTask, TaskWithAttemptStatus}, task_attempt::{TaskAttempt, CreateTaskAttempt, TaskAttemptStatus}, task_attempt_activity::{TaskAttemptActivity, CreateTaskAttemptActivity} }; @@ -22,8 +22,8 @@ pub async fn get_project_tasks( _auth: AuthUser, Path(project_id): Path, Extension(pool): Extension -) -> Result>>, StatusCode> { - match Task::find_by_project_id(&pool, project_id).await { +) -> Result>>, StatusCode> { + match Task::find_by_project_id_with_attempt_status(&pool, project_id).await { Ok(tasks) => Ok(ResponseJson(ApiResponse { success: true, data: Some(tasks), diff --git a/frontend/src/components/tasks/TaskCard.tsx b/frontend/src/components/tasks/TaskCard.tsx index 655dd86c..6cbc43ac 100644 --- a/frontend/src/components/tasks/TaskCard.tsx +++ b/frontend/src/components/tasks/TaskCard.tsx @@ -6,18 +6,10 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { KanbanCard } from '@/components/ui/shadcn-io/kanban' -import { MoreHorizontal, Trash2, Edit } from 'lucide-react' -import type { TaskStatus } from 'shared/types' +import { MoreHorizontal, Trash2, Edit, Loader2 } from 'lucide-react' +import type { TaskWithAttemptStatus } from 'shared/types' -interface Task { - id: string - project_id: string - title: string - description: string | null - status: TaskStatus - created_at: string - updated_at: string -} +type Task = TaskWithAttemptStatus interface TaskCardProps { task: Task @@ -46,6 +38,10 @@ export function TaskCard({ task, index, status, onEdit, onDelete, onViewDetails
+ {/* In Progress Spinner */} + {task.has_in_progress_attempt && ( + + )} {/* Actions Menu */}
e.stopPropagation()} diff --git a/frontend/src/components/tasks/TaskKanbanBoard.tsx b/frontend/src/components/tasks/TaskKanbanBoard.tsx index 0be2596e..bc5eca9f 100644 --- a/frontend/src/components/tasks/TaskKanbanBoard.tsx +++ b/frontend/src/components/tasks/TaskKanbanBoard.tsx @@ -6,17 +6,9 @@ import { type DragEndEvent } from '@/components/ui/shadcn-io/kanban' import { TaskCard } from './TaskCard' -import type { TaskStatus } from 'shared/types' +import type { TaskStatus, TaskWithAttemptStatus } from 'shared/types' -interface Task { - id: string - project_id: string - title: string - description: string | null - status: TaskStatus - created_at: string - updated_at: string -} +type Task = TaskWithAttemptStatus interface TaskKanbanBoardProps { tasks: Task[] diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index e771ee3c..657a670d 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -8,18 +8,10 @@ import { TaskCreateDialog } from '@/components/tasks/TaskCreateDialog' import { TaskEditDialog } from '@/components/tasks/TaskEditDialog' import { TaskDetailsDialog } from '@/components/tasks/TaskDetailsDialog' import { TaskKanbanBoard } from '@/components/tasks/TaskKanbanBoard' -import type { TaskStatus } from 'shared/types' +import type { TaskStatus, TaskWithAttemptStatus } from 'shared/types' import type { DragEndEvent } from '@/components/ui/shadcn-io/kanban' -interface Task { - id: string - project_id: string - title: string - description: string | null - status: TaskStatus - created_at: string - updated_at: string -} +type Task = TaskWithAttemptStatus interface Project { id: string diff --git a/shared/types.ts b/shared/types.ts index ba0f7a50..8ea424fc 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -17,6 +17,8 @@ export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelle export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, }; +export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, has_in_progress_attempt: boolean, }; + export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, }; export type TaskAttemptStatus = "init" | "inprogress" | "paused";