Add in-progress state to tasks

Introduce IN_PROGRESS as a new task state between TODO and DONE across
the full stack: database enum, Go backend, GraphQL, MCP, and frontend.

The task state icon now cycles forward on click (TODO → IN_PROGRESS →
DONE → TODO), and the action dropdown provides explicit "Move to"
options for any state transition. The "All" tab supports drag-and-drop
between state sections to change a task's state.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-02 15:50:20 +02:00
parent 5c71575ade
commit 8adf26ad20
9 changed files with 340 additions and 169 deletions

View File

@@ -27,11 +27,12 @@ import {
PriorityLevel,
PropertyRow,
Select,
TaskStateIcon,
Textarea,
useDialogRef,
} from "@probo/ui";
import { Breadcrumb } from "@probo/ui";
import type { ReactNode } from "react";
import { type ReactNode, useEffect } from "react";
import { Controller } from "react-hook-form";
import { useFragment, useRelayEnvironment } from "react-relay";
import { graphql } from "relay-runtime";
@@ -50,6 +51,7 @@ const taskFragment = graphql`
id
description
name
state
priority
timeEstimate
deadline
@@ -91,6 +93,7 @@ export const taskUpdateMutation = graphql`
}
`;
export const taskStates = ["TODO", "IN_PROGRESS", "DONE"] as const;
export const taskPriorities = ["URGENT", "HIGH", "MEDIUM", "LOW"] as const;
const createTaskSchema = z.object({
@@ -109,6 +112,7 @@ const createTaskSchema = z.object({
const updateTaskSchema = z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
state: z.enum(taskStates),
priority: z.enum(taskPriorities),
timeEstimate: z.string().optional().nullable(),
assignedToId: z.preprocess(
@@ -154,6 +158,7 @@ export default function TaskFormDialog(props: Props) {
defaultValues: {
name: task?.name ?? "",
description: task?.description ?? "",
state: task?.state ?? "TODO",
priority: task?.priority ?? "MEDIUM",
timeEstimate: task?.timeEstimate ?? "",
assignedToId: task?.assignedTo?.id ?? "",
@@ -162,6 +167,23 @@ export default function TaskFormDialog(props: Props) {
},
});
useEffect(() => {
if (task) {
reset({
name: task.name,
description: task.description ?? "",
state: task.state,
priority: task.priority,
timeEstimate: task.timeEstimate ?? "",
assignedToId: task.assignedTo?.id ?? "",
measureId: task.measure?.id ?? measureId ?? "",
deadline: task.deadline?.split("T")[0] ?? "",
});
}
}, [
task, reset, measureId,
]);
const onSubmit = async (data: z.infer<typeof updateTaskSchema | typeof createTaskSchema>) => {
if (task) {
await mutate({
@@ -170,6 +192,7 @@ export default function TaskFormDialog(props: Props) {
taskId: task.id,
name: data.name,
description: data.description || null,
state: "state" in data ? data.state : undefined,
priority: data.priority,
timeEstimate: data.timeEstimate || null,
deadline: formatDatetime(data.deadline) ?? null,
@@ -242,6 +265,42 @@ export default function TaskFormDialog(props: Props) {
{/* Properties form */}
<div className="py-5 px-6 bg-subtle">
<Label>{__("Properties")}</Label>
{isUpdating && (
<PropertyRow
label={__("State")}
error={"state" in formState.errors ? formState.errors.state?.message : undefined}
>
<Controller
name="state"
control={control}
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
<Option value="TODO">
<span className="flex items-center gap-2">
<TaskStateIcon state="TODO" />
{__("To do")}
</span>
</Option>
<Option value="IN_PROGRESS">
<span className="flex items-center gap-2">
<TaskStateIcon state="IN_PROGRESS" />
{__("In progress")}
</span>
</Option>
<Option value="DONE">
<span className="flex items-center gap-2">
<TaskStateIcon state="DONE" />
{__("Done")}
</span>
</Option>
</Select>
)}
/>
</PropertyRow>
)}
<PropertyRow
label={__("Priority")}
error={formState.errors.priority?.message}