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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user