From c547e556ecf02d40759e9a8aff1cf02ee455beac Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Wed, 28 Oct 2026 05:49:42 +0100 Subject: [PATCH] Open a task in its own modal from its title, with status moves, and edit its brief in the code editor The modal shows who has the task and where, who added it and when it moved, its files and notes (each note naming its bot). The task form's fields all show their labels. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- src/components/general/task-detail-modal.tsx | 92 +++++++++++++ src/components/general/task-detail-panel.tsx | 112 +++++++++++++--- src/components/general/task-fact-row.tsx | 22 ++++ src/components/general/task-form.tsx | 73 +++++++---- src/components/general/task-list-item.tsx | 38 ++---- src/components/general/task-note-row.tsx | 31 +++++ .../general/task-status-move-button.tsx | 28 ++++ src/components/general/task-status-moves.tsx | 36 ++++++ src/functions/backend/db/tasks/grab-task.ts | 18 ++- .../[bot_id]/(partials)/bot-tasks-modal.tsx | 114 ++++++----------- .../(partials)/workspace-tasks-group.tsx | 121 ++++++------------ src/types/index.ts | 15 ++- 13 files changed, 462 insertions(+), 240 deletions(-) create mode 100644 src/components/general/task-detail-modal.tsx create mode 100644 src/components/general/task-fact-row.tsx create mode 100644 src/components/general/task-note-row.tsx create mode 100644 src/components/general/task-status-move-button.tsx create mode 100644 src/components/general/task-status-moves.tsx diff --git a/AGENTS.md b/AGENTS.md index 1197afc..f7077c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,7 +153,7 @@ A secret URL, `POST /api/hooks/` (`src/pages/api/hooks/[token].ts`, publi ### Tasks (the to-do tracker) -`tasks` (a bot's own list is `bot_id` alone; a workspace's task has `workspace_id` plus the `bot_id` that has it there, null while shared; a bot reaches its own list and its own plus shared tasks in its workspaces, a workspace admin all of that workspace's, `botTaskScopesSql`; a shared task becomes the bot's when it starts it; removing or deleting a bot makes its workspace tasks shared again), `task_notes`, `task_media` (deliverable|reference). Not the same as `bot_tasks`/`bots_queue` (those record jobs). Visibility SQL in `src/utils/task-access/`. Helpers in `src/functions/backend/db/tasks/`; tools `list_tasks`/`create_task`/`update_task`; API `api/admin/tasks/` (`userAuth` -> `isReadOnlyUser` -> `assertUserReachesTask` -> `safeErrorMessage`). UI in the workspace dashboard and the header's Tasks modal (shared components in `src/components/general/`). **Hand-over:** `handOverTask` (bot: `delegate_task` with `task_id`; UI: Hand over, crew managers) moves an open task to the new bot (a workspace's stays in the workspace, now the new bot's, and is worked there), sets it doing and queues a job with `params.tracker_task_id`; `noteTrackerTask` notes the job's end on it and blocks it on failure. **Not built yet:** task steps, `server:task-changed` pushes, a `/admin/tasks` page. +`tasks` (a bot's own list is `bot_id` alone; a workspace's task has `workspace_id` plus the `bot_id` that has it there, null while shared; a bot reaches its own list and its own plus shared tasks in its workspaces, a workspace admin all of that workspace's, `botTaskScopesSql`; a shared task becomes the bot's when it starts it; removing or deleting a bot makes its workspace tasks shared again), `task_notes`, `task_media` (deliverable|reference). Not the same as `bot_tasks`/`bots_queue` (those record jobs). Visibility SQL in `src/utils/task-access/`. Helpers in `src/functions/backend/db/tasks/`; tools `list_tasks`/`create_task`/`update_task`; API `api/admin/tasks/` (`userAuth` -> `isReadOnlyUser` -> `assertUserReachesTask` -> `safeErrorMessage`). UI in the workspace dashboard and the header's Tasks modal (shared components in `src/components/general/`); a card's title opens `TaskDetailModal` with everything about the task and its status moves (a modal over the list, never swapped in place). **Hand-over:** `handOverTask` (bot: `delegate_task` with `task_id`; UI: Hand over, crew managers) moves an open task to the new bot (a workspace's stays in the workspace, now the new bot's, and is worked there), sets it doing and queues a job with `params.tracker_task_id`; `noteTrackerTask` notes the job's end on it and blocks it on failure. **Not built yet:** task steps, `server:task-changed` pushes, a `/admin/tasks` page. ### Envs diff --git a/src/components/general/task-detail-modal.tsx b/src/components/general/task-detail-modal.tsx new file mode 100644 index 0000000..95efe15 --- /dev/null +++ b/src/components/general/task-detail-modal.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from "react"; +import Span from "@moduletrace/twui/layout/Span"; +import Stack from "@moduletrace/twui/layout/Stack"; +import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi"; +import type { APIResponseObject } from "@moduletrace/bunext/types"; +import type { TaskDetail, TaskWithNames } from "@/src/types"; +import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell"; +import taskPlaceLabel from "@/src/utils/task-place-label"; +import TaskDetailPanel from "./task-detail-panel"; + +type Props = { + /** + * The list row clicked; open while set. Its notes and files load when + * it opens, so the list stays small. + */ + task?: TaskWithNames | null; + /** + * A crew manager can change it, file onto it and delete it. + */ + can_manage?: boolean; + onClose: () => void; + onChanged: (params: { task: TaskDetail; msg?: string }) => void; + onDeleted: (params: { task_id: number }) => void; +}; + +/** + * A task opened from a list in a modal of its own: everything about it, and + * for a crew manager its edit, hand-over, files and delete. + */ +export default function TaskDetailModal({ task, can_manage, onClose, onChanged, onDeleted }: Props) { + const [detail, set_detail] = useState(null); + const [error, set_error] = useState(); + const task_id = task?.id ? Number(task.id) : 0; + + useEffect(() => { + set_detail(null); + set_error(undefined); + + if (!task_id) return; + + let live = true; + + fetchApi<{}, APIResponseObject>(`/api/admin/tasks/get-task?task_id=${task_id}`, { + method: "GET", + }) + .catch(() => null) + .then((res) => { + if (!live) return; + + if (!res?.success || !res.singleRes) { + set_error(res?.msg || "Couldn't open the task."); + return; + } + + set_detail(res.singleRes); + }); + + return () => { + live = false; + }; + }, [task_id]); + + const shown = detail || task; + + return ( + !open && onClose()} + title={shown?.title || (task_id ? `Task ${task_id}` : "Task")} + description={shown ? taskPlaceLabel({ task: shown }) : undefined} + > + + {detail ? ( + { + set_detail(saved); + onChanged({ task: saved, msg }); + }} + onDeleted={() => onDeleted({ task_id })} + /> + ) : ( + + {error || "Loading the task..."} + + )} + + + ); +} diff --git a/src/components/general/task-detail-panel.tsx b/src/components/general/task-detail-panel.tsx index efc6fdb..5a88913 100644 --- a/src/components/general/task-detail-panel.tsx +++ b/src/components/general/task-detail-panel.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useContext, useRef, useState } from "react"; import Button from "@moduletrace/twui/layout/Button"; import Row from "@moduletrace/twui/layout/Row"; import Span from "@moduletrace/twui/layout/Span"; @@ -10,6 +10,14 @@ import TaskFileRow from "@/src/components/general/task-file-row"; import HandOverForm from "@/src/components/general/hand-over-form"; import TaskForm from "@/src/components/general/task-form"; import TaskStatusChip from "@/src/components/general/task-status-chip"; +import TaskStatusMoves from "@/src/components/general/task-status-moves"; +import BotAvatar from "@/src/components/general/bot-avatar"; +import TaskFactRow from "@/src/components/general/task-fact-row"; +import TaskNoteRow from "@/src/components/general/task-note-row"; +import type { BotFace } from "@/src/dict/bot-dict"; +import { AppContext } from "@/src/pages/__root"; +import botStoneProps from "@/src/utils/bot-stone-props"; +import relativeTime from "@/src/utils/relative-time"; import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi"; import type { APIResponseObject } from "@moduletrace/bunext/types"; @@ -24,12 +32,14 @@ type Props = { }; /** - * One task opened: what it is, its notes oldest first, its files (each - * opening its bytes), a box to add a note, and edit or delete. A crew + * One task opened: its status (a crew manager moves it along), who has it and where, who added it and + * when it last moved, what it is, its files (each opening its bytes), its + * notes oldest first, a box to add a note, and edit or delete. A crew * manager can file a new deliverable straight onto it, or hand an open * one to another bot, which starts on it. */ export default function TaskDetailPanel({ task, can_manage, onChanged, onDeleted }: Props) { + const { pageProps } = useContext(AppContext); const [editing, setEditing] = useState(false); const [handing_over, setHandingOver] = useState(false); const [note, setNote] = useState(""); @@ -135,13 +145,21 @@ export default function TaskDetailPanel({ task, can_manage, onChanged, onDeleted - - {task.workspace_id - ? task.workspace_name || `Workspace ${task.workspace_id}` - : task.bot_name || `Bot ${task.bot_id}`} - {can_manage ? ( - + + run({ + path: "tasks/save-task", + body: { task_id: Number(task.id), status: next }, + done: "Status changed.", + }) + } + /> + ) : null} + {can_manage ? ( + {(OpenTaskStatuses as readonly string[]).includes(task.status || "") ? (