Distinguish in progress
This commit is contained in:
@@ -71,6 +71,8 @@ export {}
|
|||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
export {}"#,
|
export {}"#,
|
||||||
bloop_backend::models::ApiResponse::<()>::decl(),
|
bloop_backend::models::ApiResponse::<()>::decl(),
|
||||||
bloop_backend::executor::ExecutorConfig::decl(),
|
bloop_backend::executor::ExecutorConfig::decl(),
|
||||||
@@ -80,6 +82,7 @@ export {}"#,
|
|||||||
bloop_backend::models::task::CreateTask::decl(),
|
bloop_backend::models::task::CreateTask::decl(),
|
||||||
bloop_backend::models::task::TaskStatus::decl(),
|
bloop_backend::models::task::TaskStatus::decl(),
|
||||||
bloop_backend::models::task::Task::decl(),
|
bloop_backend::models::task::Task::decl(),
|
||||||
|
bloop_backend::models::task::TaskWithAttemptStatus::decl(),
|
||||||
bloop_backend::models::task::UpdateTask::decl(),
|
bloop_backend::models::task::UpdateTask::decl(),
|
||||||
bloop_backend::models::task_attempt::TaskAttemptStatus::decl(),
|
bloop_backend::models::task_attempt::TaskAttemptStatus::decl(),
|
||||||
bloop_backend::models::task_attempt::TaskAttempt::decl(),
|
bloop_backend::models::task_attempt::TaskAttempt::decl(),
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ pub struct Task {
|
|||||||
pub updated_at: DateTime<Utc>,
|
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)]
|
#[derive(Debug, Deserialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct CreateTask {
|
pub struct CreateTask {
|
||||||
@@ -58,6 +71,51 @@ impl Task {
|
|||||||
.await
|
.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> {
|
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Task,
|
Task,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use uuid::Uuid;
|
|||||||
use crate::models::{
|
use crate::models::{
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
project::Project,
|
project::Project,
|
||||||
task::{Task, CreateTask, UpdateTask},
|
task::{Task, CreateTask, UpdateTask, TaskWithAttemptStatus},
|
||||||
task_attempt::{TaskAttempt, CreateTaskAttempt, TaskAttemptStatus},
|
task_attempt::{TaskAttempt, CreateTaskAttempt, TaskAttemptStatus},
|
||||||
task_attempt_activity::{TaskAttemptActivity, CreateTaskAttemptActivity}
|
task_attempt_activity::{TaskAttemptActivity, CreateTaskAttemptActivity}
|
||||||
};
|
};
|
||||||
@@ -22,8 +22,8 @@ pub async fn get_project_tasks(
|
|||||||
_auth: AuthUser,
|
_auth: AuthUser,
|
||||||
Path(project_id): Path<Uuid>,
|
Path(project_id): Path<Uuid>,
|
||||||
Extension(pool): Extension<PgPool>
|
Extension(pool): Extension<PgPool>
|
||||||
) -> Result<ResponseJson<ApiResponse<Vec<Task>>>, StatusCode> {
|
) -> Result<ResponseJson<ApiResponse<Vec<TaskWithAttemptStatus>>>, StatusCode> {
|
||||||
match Task::find_by_project_id(&pool, project_id).await {
|
match Task::find_by_project_id_with_attempt_status(&pool, project_id).await {
|
||||||
Ok(tasks) => Ok(ResponseJson(ApiResponse {
|
Ok(tasks) => Ok(ResponseJson(ApiResponse {
|
||||||
success: true,
|
success: true,
|
||||||
data: Some(tasks),
|
data: Some(tasks),
|
||||||
|
|||||||
@@ -6,18 +6,10 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { KanbanCard } from '@/components/ui/shadcn-io/kanban'
|
import { KanbanCard } from '@/components/ui/shadcn-io/kanban'
|
||||||
import { MoreHorizontal, Trash2, Edit } from 'lucide-react'
|
import { MoreHorizontal, Trash2, Edit, Loader2 } from 'lucide-react'
|
||||||
import type { TaskStatus } from 'shared/types'
|
import type { TaskWithAttemptStatus } from 'shared/types'
|
||||||
|
|
||||||
interface Task {
|
type Task = TaskWithAttemptStatus
|
||||||
id: string
|
|
||||||
project_id: string
|
|
||||||
title: string
|
|
||||||
description: string | null
|
|
||||||
status: TaskStatus
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: Task
|
task: Task
|
||||||
@@ -46,6 +38,10 @@ export function TaskCard({ task, index, status, onEdit, onDelete, onViewDetails
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1">
|
<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 */}
|
{/* Actions Menu */}
|
||||||
<div
|
<div
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
|||||||
@@ -6,17 +6,9 @@ import {
|
|||||||
type DragEndEvent
|
type DragEndEvent
|
||||||
} from '@/components/ui/shadcn-io/kanban'
|
} from '@/components/ui/shadcn-io/kanban'
|
||||||
import { TaskCard } from './TaskCard'
|
import { TaskCard } from './TaskCard'
|
||||||
import type { TaskStatus } from 'shared/types'
|
import type { TaskStatus, TaskWithAttemptStatus } from 'shared/types'
|
||||||
|
|
||||||
interface Task {
|
type Task = TaskWithAttemptStatus
|
||||||
id: string
|
|
||||||
project_id: string
|
|
||||||
title: string
|
|
||||||
description: string | null
|
|
||||||
status: TaskStatus
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskKanbanBoardProps {
|
interface TaskKanbanBoardProps {
|
||||||
tasks: Task[]
|
tasks: Task[]
|
||||||
|
|||||||
@@ -8,18 +8,10 @@ import { TaskCreateDialog } from '@/components/tasks/TaskCreateDialog'
|
|||||||
import { TaskEditDialog } from '@/components/tasks/TaskEditDialog'
|
import { TaskEditDialog } from '@/components/tasks/TaskEditDialog'
|
||||||
import { TaskDetailsDialog } from '@/components/tasks/TaskDetailsDialog'
|
import { TaskDetailsDialog } from '@/components/tasks/TaskDetailsDialog'
|
||||||
import { TaskKanbanBoard } from '@/components/tasks/TaskKanbanBoard'
|
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'
|
import type { DragEndEvent } from '@/components/ui/shadcn-io/kanban'
|
||||||
|
|
||||||
interface Task {
|
type Task = TaskWithAttemptStatus
|
||||||
id: string
|
|
||||||
project_id: string
|
|
||||||
title: string
|
|
||||||
description: string | null
|
|
||||||
status: TaskStatus
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string
|
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 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 UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, };
|
||||||
|
|
||||||
export type TaskAttemptStatus = "init" | "inprogress" | "paused";
|
export type TaskAttemptStatus = "init" | "inprogress" | "paused";
|
||||||
|
|||||||
Reference in New Issue
Block a user