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 <[email protected]>
This commit is contained in:
2026-10-28 05:49:42 +01:00
co-authored by Claude Opus 5.5
parent 74da091dd4
commit c547e556ec
13 changed files with 462 additions and 240 deletions
@@ -10,7 +10,7 @@ import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
import type { BotWithLastMessage, TaskDetail, TaskWithNames } from "@/src/types";
import TaskDetailPanel from "@/src/components/general/task-detail-panel";
import TaskDetailModal from "@/src/components/general/task-detail-modal";
import TaskForm from "@/src/components/general/task-form";
import TaskListItem from "@/src/components/general/task-list-item";
@@ -33,13 +33,14 @@ type Props = {
* The tasks this conversation works on: a bot's own list, or in a workspace
* the ones it has there plus the shared ones nobody has yet. Open it from the header menu; the list loads
* then, so the page's props stay small. A crew manager adds, moves, edits
* and deletes; a task opens to show its notes and files.
* and deletes; a card opens the task in a modal of its own with everything
* about it.
*/
export default function BotTasksModal({ bot, workspace, menu, on_open }: Props) {
const { pageProps } = useContext(AppContext);
const [open, setOpen] = useState(false);
const [adding, set_adding] = useState(false);
const [open_task, set_open_task] = useState<TaskDetail | null>(null);
const [open_task, set_open_task] = useState<TaskWithNames | null>(null);
const [busy, set_busy] = useState(false);
const [status, set_status] = useState<string>();
const can_manage = canManageCrew({ user_types: pageProps?.user_types });
@@ -103,26 +104,6 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props)
set_status("Task deleted.");
};
const show = async ({ task }: { task: TaskWithNames }) => {
set_status(undefined);
/**
* The list already has the task; the detail endpoint serves its
* notes and files.
*/
const detail = await fetchApi<{}, APIResponseObject<TaskDetail>>(
`/api/admin/tasks/get-task?task_id=${task.id}`,
{ method: "GET" },
).catch(() => null);
if (!detail?.success || !detail.singleRes) {
set_status(detail?.msg || "Couldn't open the task.");
return;
}
set_open_task(detail.singleRes);
};
return (
<Modal
target={
@@ -159,72 +140,30 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props)
<Stack className="w-full gap-1! items-stretch">
<Row className="w-full gap-2! flex-nowrap items-center">
<H2 className="grow text-lg! leading-6! tracking-normal! font-bold text-ink">
{open_task ? open_task.title || "Task" : title}
{title}
</H2>
{open_task ? (
<Button
title="Back to the list"
type="button"
variant="ghost"
color="gray"
size="small"
onClick={() => set_open_task(null)}
className="px-2! text-[12px]"
>
Back
</Button>
) : can_manage ? (
{can_manage ? (
<Button
title="Add a task"
type="button"
variant="outlined"
color="gray"
size="small"
onClick={() => {
set_adding(true);
set_open_task(null);
}}
onClick={() => set_adding(true)}
>
Add task
</Button>
) : null}
</Row>
{!open_task ? (
<Span className="text-[13px] leading-[18px] text-ink-secondary">
{workspace?.id
? `What ${name} has in ${workspace.name || "this workspace"}, and the shared tasks there nobody has taken up yet. The workspace shows every bot's.`
: `What ${name} is working through, and what it's waiting on.`}
</Span>
) : null}
<Span className="text-[13px] leading-[18px] text-ink-secondary">
{workspace?.id
? `What ${name} has in ${workspace.name || "this workspace"}, and the shared tasks there nobody has taken up yet. The workspace shows every bot's.`
: `What ${name} is working through, and what it's waiting on.`}
</Span>
</Stack>
<Stack className="w-full mt-4 gap-0! items-stretch overflow-y-auto max-h-[55vh]">
{open_task ? (
<TaskDetailPanel
task={open_task}
can_manage={can_manage}
onChanged={(saved, msg) => {
set_status(msg);
/**
* Handed to another bot, it's that bot's now.
*/
if (saved.bot_id && saved.bot_id != bot.id) {
setTasks((prev) => prev.filter((item) => item.id != saved.id));
set_open_task(null);
return;
}
set_open_task(saved);
replace({ task: saved });
}}
onDeleted={() => {
setTasks((prev) => prev.filter((item) => item.id != open_task.id));
set_open_task(null);
set_status("Task deleted.");
}}
/>
) : adding ? (
{adding ? (
<TaskForm
bot_id={Number(bot.id)}
workspace_id={workspace?.id ? Number(workspace.id) : null}
@@ -241,7 +180,7 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props)
key={task.id}
task={task}
busy={busy}
onOpen={() => show({ task }).catch(() => {})}
onOpen={() => set_open_task(task)}
onStatusChange={(next) => move({ task, next })}
onDelete={() => remove({ task }).catch(() => {})}
/>
@@ -258,6 +197,31 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props)
{status}
</Span>
) : null}
<TaskDetailModal
task={open_task}
can_manage={can_manage}
onClose={() => set_open_task(null)}
onChanged={({ task: saved, msg }) => {
set_status(msg);
/**
* Handed to another bot, it's that bot's now.
*/
if (saved.bot_id && saved.bot_id != bot.id) {
setTasks((prev) => prev.filter((item) => item.id != saved.id));
set_open_task(null);
return;
}
replace({ task: saved });
}}
onDeleted={({ task_id }) => {
setTasks((prev) => prev.filter((item) => item.id != task_id));
set_open_task(null);
set_status("Task deleted.");
}}
/>
</Modal>
);
}
@@ -6,7 +6,7 @@ import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import Span from "@moduletrace/twui/layout/Span";
import type { TaskDetail, TaskWithNames } from "@/src/types";
import TaskDetailPanel from "@/src/components/general/task-detail-panel";
import TaskDetailModal from "@/src/components/general/task-detail-modal";
import TaskForm from "@/src/components/general/task-form";
import TaskListItem from "@/src/components/general/task-list-item";
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
@@ -35,7 +35,7 @@ export default function WorkspaceTasksGroup({ workspace_id, tasks }: Props) {
const [busy, setBusy] = useState(false);
const [status, setStatus] = useState<{ error?: boolean; success?: boolean; msg: string }>();
const [adding, setAdding] = useState(false);
const [open_task, setOpenTask] = useState<TaskDetail | null>(null);
const [open_task, setOpenTask] = useState<TaskWithNames | null>(null);
const can_manage = canManageCrew({ user_types: pageProps?.user_types });
const replace = ({ task }: { task: TaskWithNames }) =>
@@ -85,22 +85,6 @@ export default function WorkspaceTasksGroup({ workspace_id, tasks }: Props) {
setStatus({ success: true, msg: "Task deleted." });
};
const show = async ({ task }: { task: TaskWithNames }) => {
setStatus(undefined);
const res = await fetchApi<{}, APIResponseObject<TaskDetail>>(
`/api/admin/tasks/get-task?task_id=${task.id}`,
{ method: "GET" },
).catch(() => null);
if (!res?.success || !res.singleRes) {
setStatus({ error: true, msg: res?.msg || "Couldn't open the task." });
return;
}
setOpenTask(res.singleRes);
};
return (
<SettingsGroup
id="tasks"
@@ -108,71 +92,48 @@ export default function WorkspaceTasksGroup({ workspace_id, tasks }: Props) {
description="Every task in this workspace, whichever bot has it. A bot sees its own here and the shared ones nobody has taken up yet."
>
<Stack className="w-full gap-2! items-stretch">
{open_task ? (
<Stack className="w-full gap-3! items-stretch">
<Row className="w-full gap-2! flex-nowrap items-center">
<Span className="grow min-w-0 text-sm font-bold leading-5 text-ink truncate">
{open_task.title || "Task"}
</Span>
<Button
title="Back to the list"
type="button"
variant="ghost"
color="gray"
size="small"
onClick={() => setOpenTask(null)}
className="px-2! text-[12px]"
>
Back
</Button>
</Row>
<TaskDetailPanel
task={open_task}
can_manage={can_manage}
onChanged={(saved, msg) => {
setOpenTask(saved);
replace({ task: saved });
setStatus(msg ? { success: true, msg } : undefined);
}}
onDeleted={() => {
setItems((prev) => prev.filter((item) => item.id != open_task.id));
setOpenTask(null);
setStatus({ success: true, msg: "Task deleted." });
}}
{can_manage ? (
<Row className="justify-end">
<Button
title="Add a task"
type="button"
variant="outlined"
color="gray"
size="small"
onClick={() => setAdding(true)}
>
Add task
</Button>
</Row>
) : null}
<SettingsList is_empty={!items.length} empty="No tasks in this workspace yet.">
{items.map((task) => (
<TaskListItem
key={task.id}
task={task}
busy={busy}
onOpen={() => setOpenTask(task)}
onStatusChange={(next) => move({ task, next })}
onDelete={() => remove({ task }).catch(() => {})}
/>
</Stack>
) : (
<>
{can_manage ? (
<Row className="justify-end">
<Button
title="Add a task"
type="button"
variant="outlined"
color="gray"
size="small"
onClick={() => setAdding(true)}
>
Add task
</Button>
</Row>
) : null}
<SettingsList is_empty={!items.length} empty="No tasks in this workspace yet.">
{items.map((task) => (
<TaskListItem
key={task.id}
task={task}
busy={busy}
onOpen={() => show({ task }).catch(() => {})}
onStatusChange={(next) => move({ task, next })}
onDelete={() => remove({ task }).catch(() => {})}
/>
))}
</SettingsList>
</>
)}
))}
</SettingsList>
<SettingsStatus status={status} />
</Stack>
<TaskDetailModal
task={open_task}
can_manage={can_manage}
onClose={() => setOpenTask(null)}
onChanged={({ task, msg }) => {
replace({ task });
setStatus(msg ? { success: true, msg } : undefined);
}}
onDeleted={({ task_id }) => {
setItems((prev) => prev.filter((item) => item.id != task_id));
setOpenTask(null);
setStatus({ success: true, msg: "Task deleted." });
}}
/>
<AddModalShell
open={adding}
setOpen={setAdding}