Distinguish in progress
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user