Distinguish in progress

This commit is contained in:
Louis Knight-Webb
2025-06-16 21:25:19 -04:00
parent 1b5155cb4c
commit 284e33511a
7 changed files with 77 additions and 34 deletions

View File

@@ -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(),

View File

@@ -28,6 +28,19 @@ pub struct Task {
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TaskWithAttemptStatus {
pub id: Uuid,
pub project_id: Uuid,
pub title: String,
pub description: Option<String>,
pub status: TaskStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
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<Vec<TaskWithAttemptStatus>, 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<Option<Self>, sqlx::Error> {
sqlx::query_as!(
Task,

View File

@@ -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<Uuid>,
Extension(pool): Extension<PgPool>
) -> Result<ResponseJson<ApiResponse<Vec<Task>>>, StatusCode> {
match Task::find_by_project_id(&pool, project_id).await {
) -> Result<ResponseJson<ApiResponse<Vec<TaskWithAttemptStatus>>>, StatusCode> {
match Task::find_by_project_id_with_attempt_status(&pool, project_id).await {
Ok(tasks) => Ok(ResponseJson(ApiResponse {
success: true,
data: Some(tasks),

View File

@@ -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
</h4>
</div>
<div className="flex items-center space-x-1">
{/* In Progress Spinner */}
{task.has_in_progress_attempt && (
<Loader2 className="h-3 w-3 animate-spin text-blue-500" />
)}
{/* Actions Menu */}
<div
onPointerDown={(e) => e.stopPropagation()}

View File

@@ -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[]

View File

@@ -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

View File

@@ -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";