Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74da091dd4 | ||
|
|
a33593460b |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cobble",
|
"name": "cobble",
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { TaskStatuses } from "@/src/dict/task-dict";
|
|||||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
||||||
import relativeTime from "@/src/utils/relative-time";
|
import relativeTime from "@/src/utils/relative-time";
|
||||||
import taskPlaceLabel from "@/src/utils/task-place-label";
|
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";
|
import TaskStatusChip from "./task-status-chip";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -24,6 +26,10 @@ type Props = {
|
|||||||
* Moves it to one status; only crew managers get the controls.
|
* Moves it to one status; only crew managers get the controls.
|
||||||
*/
|
*/
|
||||||
onStatusChange?: (status: string) => void;
|
onStatusChange?: (status: string) => void;
|
||||||
|
/**
|
||||||
|
* Deletes it after asking; only crew managers get the button.
|
||||||
|
*/
|
||||||
|
onDelete?: () => void;
|
||||||
busy?: boolean;
|
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
|
* is and who has it, what it's blocked on, its latest note and how long ago it
|
||||||
* moved.
|
* 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 { pageProps } = useContext(AppContext);
|
||||||
const can_manage = canManageCrew({ user_types: pageProps?.user_types });
|
const can_manage = canManageCrew({ user_types: pageProps?.user_types });
|
||||||
const done = task.status == "done" || task.status == "cancelled";
|
const done = task.status == "done" || task.status == "cancelled";
|
||||||
@@ -61,6 +67,14 @@ export default function TaskListItem({ task, onOpen, onStatusChange, busy }: Pro
|
|||||||
<Span className="truncate">{task.title || `Task ${task.id}`}</Span>
|
<Span className="truncate">{task.title || `Task ${task.id}`}</Span>
|
||||||
</Button>
|
</Button>
|
||||||
<TaskStatusChip status={task.status} />
|
<TaskStatusChip status={task.status} />
|
||||||
|
{can_manage && onDelete ? (
|
||||||
|
<RowIconButton
|
||||||
|
icon={Trash2}
|
||||||
|
title={`Delete ${task.title || `task ${task.id}`}`}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={onDelete}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="w-full gap-2! flex-nowrap items-center">
|
<Row className="w-full gap-2! flex-nowrap items-center">
|
||||||
<Span className="shrink-0 text-xs leading-4 text-ink-secondary truncate">
|
<Span className="shrink-0 text-xs leading-4 text-ink-secondary truncate">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import Tag from "@moduletrace/twui/elements/Tag";
|
import Tag from "@moduletrace/twui/elements/Tag";
|
||||||
import { TaskStatuses, type TaskStatus } from "@/src/dict/task-dict";
|
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 value = (status || "todo") as TaskStatus;
|
||||||
const title = TaskStatuses.find((it) => it.value == value)?.title || "To do";
|
const title = TaskStatuses.find((it) => it.value == value)?.title || "To do";
|
||||||
|
|
||||||
const tone =
|
/**
|
||||||
value == "blocked"
|
* A `Tag` is a filled primary button by default (ink in light mode), so
|
||||||
? "text-ink border-ink"
|
* the chip clears its fill and sets its own text: only the outline speaks.
|
||||||
: value == "doing"
|
*/
|
||||||
? "text-ink border-hairline-strong"
|
|
||||||
: "text-ink-secondary border-hairline";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag className={`shrink-0 px-2! py-0.5! text-[11px]! font-bold! leading-4! border border-solid ${tone}`}>
|
<Tag
|
||||||
|
className={twMerge(
|
||||||
|
"shrink-0 px-2! py-0.5! text-[11px]! font-bold! leading-4! border border-solid",
|
||||||
|
"bg-transparent! dark:bg-transparent! hover:bg-transparent!",
|
||||||
|
value == "blocked" && "text-ink! dark:text-ink! border-ink",
|
||||||
|
value == "doing" && "text-ink! dark:text-ink! border-hairline-strong",
|
||||||
|
value != "blocked" && value != "doing" && "text-ink-secondary! dark:text-ink-secondary! border-hairline",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -80,6 +80,29 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props)
|
|||||||
replace({ task: res.singleRes });
|
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<Record<string, unknown>, 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 }) => {
|
const show = async ({ task }: { task: TaskWithNames }) => {
|
||||||
set_status(undefined);
|
set_status(undefined);
|
||||||
|
|
||||||
@@ -220,6 +243,7 @@ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props)
|
|||||||
busy={busy}
|
busy={busy}
|
||||||
onOpen={() => show({ task }).catch(() => {})}
|
onOpen={() => show({ task }).catch(() => {})}
|
||||||
onStatusChange={(next) => move({ task, next })}
|
onStatusChange={(next) => move({ task, next })}
|
||||||
|
onDelete={() => remove({ task }).catch(() => {})}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -62,6 +62,29 @@ export default function WorkspaceTasksGroup({ workspace_id, tasks }: Props) {
|
|||||||
replace({ task: res.singleRes });
|
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<Record<string, unknown>, 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 }) => {
|
const show = async ({ task }: { task: TaskWithNames }) => {
|
||||||
setStatus(undefined);
|
setStatus(undefined);
|
||||||
|
|
||||||
@@ -142,6 +165,7 @@ export default function WorkspaceTasksGroup({ workspace_id, tasks }: Props) {
|
|||||||
busy={busy}
|
busy={busy}
|
||||||
onOpen={() => show({ task }).catch(() => {})}
|
onOpen={() => show({ task }).catch(() => {})}
|
||||||
onStatusChange={(next) => move({ task, next })}
|
onStatusChange={(next) => move({ task, next })}
|
||||||
|
onDelete={() => remove({ task }).catch(() => {})}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
|
|||||||
Reference in New Issue
Block a user