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(() => {})}
/>
))}