diff --git a/src/components/general/task-list-item.tsx b/src/components/general/task-list-item.tsx index 442239f..7f6b62b 100644 --- a/src/components/general/task-list-item.tsx +++ b/src/components/general/task-list-item.tsx @@ -12,6 +12,8 @@ import { TaskStatuses } from "@/src/dict/task-dict"; import canManageCrew from "@/src/utils/rbac/can-manage-crew"; import relativeTime from "@/src/utils/relative-time"; import taskPlaceLabel from "@/src/utils/task-place-label"; +import { Trash2 } from "lucide-react"; +import RowIconButton from "./row-icon-button"; import TaskStatusChip from "./task-status-chip"; type Props = { @@ -24,6 +26,10 @@ type Props = { * Moves it to one status; only crew managers get the controls. */ onStatusChange?: (status: string) => void; + /** + * Deletes it after asking; only crew managers get the button. + */ + onDelete?: () => void; busy?: boolean; }; @@ -32,7 +38,7 @@ type Props = { * is and who has it, what it's blocked on, its latest note and how long ago it * moved. */ -export default function TaskListItem({ task, onOpen, onStatusChange, busy }: Props) { +export default function TaskListItem({ task, onOpen, onStatusChange, onDelete, busy }: Props) { const { pageProps } = useContext(AppContext); const can_manage = canManageCrew({ user_types: pageProps?.user_types }); const done = task.status == "done" || task.status == "cancelled"; @@ -61,6 +67,14 @@ export default function TaskListItem({ task, onOpen, onStatusChange, busy }: Pro {task.title || `Task ${task.id}`} + {can_manage && onDelete ? ( + + ) : null} diff --git a/src/components/general/task-status-chip.tsx b/src/components/general/task-status-chip.tsx index 00b1aae..4edcfb7 100644 --- a/src/components/general/task-status-chip.tsx +++ b/src/components/general/task-status-chip.tsx @@ -1,3 +1,4 @@ +import { twMerge } from "tailwind-merge"; import Tag from "@moduletrace/twui/elements/Tag"; import { TaskStatuses, type TaskStatus } from "@/src/dict/task-dict"; @@ -14,15 +15,20 @@ export default function TaskStatusChip({ status = "todo" }: Props) { const value = (status || "todo") as TaskStatus; const title = TaskStatuses.find((it) => it.value == value)?.title || "To do"; - const tone = - value == "blocked" - ? "text-ink border-ink" - : value == "doing" - ? "text-ink border-hairline-strong" - : "text-ink-secondary border-hairline"; - + /** + * A `Tag` is a filled primary button by default (ink in light mode), so + * the chip clears its fill and sets its own text: only the outline speaks. + */ return ( - + {title} ); diff --git a/src/pages/admin/bots/[bot_id]/(partials)/bot-tasks-modal.tsx b/src/pages/admin/bots/[bot_id]/(partials)/bot-tasks-modal.tsx index b870cb5..f2b34fc 100644 --- a/src/pages/admin/bots/[bot_id]/(partials)/bot-tasks-modal.tsx +++ b/src/pages/admin/bots/[bot_id]/(partials)/bot-tasks-modal.tsx @@ -80,6 +80,29 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props) replace({ task: res.singleRes }); }; + const remove = async ({ task }: { task: TaskWithNames }) => { + if (busy) return; + if (!window.confirm(`Delete "${task.title || `task ${task.id}`}"? Its notes and links go with it.`)) return; + + set_busy(true); + set_status(undefined); + + const res = await fetchApi, APIResponseObject>( + "/api/admin/tasks/delete-task", + { method: "POST", body: { task_id: Number(task.id) } }, + ); + + set_busy(false); + + if (!res.success) { + set_status(res.msg || "That didn't work."); + return; + } + + setTasks((prev) => prev.filter((item) => item.id != task.id)); + set_status("Task deleted."); + }; + const show = async ({ task }: { task: TaskWithNames }) => { set_status(undefined); @@ -220,6 +243,7 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props) busy={busy} onOpen={() => show({ task }).catch(() => {})} onStatusChange={(next) => move({ task, next })} + onDelete={() => remove({ task }).catch(() => {})} /> )) ) : ( diff --git a/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-tasks-group.tsx b/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-tasks-group.tsx index 9177260..256dbfd 100644 --- a/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-tasks-group.tsx +++ b/src/pages/admin/workspaces/[workspace_id]/(partials)/workspace-tasks-group.tsx @@ -62,6 +62,29 @@ export default function WorkspaceTasksGroup({ workspace_id, tasks }: Props) { replace({ task: res.singleRes }); }; + const remove = async ({ task }: { task: TaskWithNames }) => { + if (busy) return; + if (!window.confirm(`Delete "${task.title || `task ${task.id}`}"? Its notes and links go with it.`)) return; + + setBusy(true); + setStatus(undefined); + + const res = await fetchApi, APIResponseObject>( + "/api/admin/tasks/delete-task", + { method: "POST", body: { task_id: Number(task.id) } }, + ); + + setBusy(false); + + if (!res.success) { + setStatus({ error: true, msg: res.msg || "That didn't work." }); + return; + } + + setItems((prev) => prev.filter((item) => item.id != task.id)); + setStatus({ success: true, msg: "Task deleted." }); + }; + const show = async ({ task }: { task: TaskWithNames }) => { setStatus(undefined); @@ -142,6 +165,7 @@ export default function WorkspaceTasksGroup({ workspace_id, tasks }: Props) { busy={busy} onOpen={() => show({ task }).catch(() => {})} onStatusChange={(next) => move({ task, next })} + onDelete={() => remove({ task }).catch(() => {})} /> ))}