Files
cobble/src/components/general/task-list-item.tsx
T
tbenandClaude Opus 5.5 c547e556ec 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]>
2026-10-28 05:49:42 +01:00

106 lines
4.8 KiB
TypeScript

import { useContext } from "react";
import { twMerge } from "tailwind-merge";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import Button from "@moduletrace/twui/layout/Button";
import type { TaskWithNames } from "@/src/types";
import { AppContext } from "@/src/pages/__root";
import BotAvatar from "@/src/components/general/bot-avatar";
import type { BotFace } from "@/src/dict/bot-dict";
import botStoneProps from "@/src/utils/bot-stone-props";
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";
import TaskStatusMoves from "./task-status-moves";
type Props = {
task: TaskWithNames;
/**
* Opens the task in its modal: everything about it, and for a crew
* manager its edit, hand-over, files and delete. The title opens it.
*/
onOpen?: () => void;
/**
* 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;
};
/**
* One task: whose it is (a bot's stone), its title and status, where it
* is and who has it, what it's blocked on, its latest note and how long ago it
* moved. Its title opens it in a modal.
*/
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";
return (
<Row className="w-full gap-3! flex-nowrap items-start px-3 py-3 border-0 border-b border-solid border-hairline last:border-b-0">
<BotAvatar
image_media_id={task.bot_image_media_id}
{...botStoneProps({ color: task.bot_color, stone: task.bot_stone, hue: task.bot_hue })}
face={(task.bot_face || "idle") as BotFace}
size={28}
/>
<Stack className="grow min-w-0 gap-0.5! items-stretch">
<Row className="w-full gap-2! flex-nowrap items-center">
<Button
title={`Open ${task.title || `task ${task.id}`}`}
type="button"
variant="ghost"
color="text"
onClick={onOpen}
className={twMerge(
"grow min-w-0 justify-start px-0! h-auto! py-0! text-sm! font-bold! leading-5! truncate text-left hover:bg-transparent! hover:underline",
done ? "text-ink-secondary! line-through" : "text-ink!",
)}
>
<Span className="truncate">{task.title || `Task ${task.id}`}</Span>
</Button>
<TaskStatusChip status={task.status} />
{can_manage && onDelete ? (
<RowIconButton
icon={Trash2}
title={`Delete ${task.title || `task ${task.id}`}`}
disabled={busy}
onClick={onDelete}
/>
) : null}
</Row>
<Row className="w-full gap-2! flex-nowrap items-center">
<Span className="shrink-0 text-xs leading-4 text-ink-secondary truncate">
{taskPlaceLabel({ task })}
</Span>
{task.status == "blocked" && task.blocked_reason ? (
<Span className="min-w-0 text-xs leading-4 text-ink truncate">
{`Waiting on you: ${task.blocked_reason}`}
</Span>
) : null}
{task.updated_at ? (
<Span className="ml-auto shrink-0 text-[11px] leading-4 text-ink-secondary">
{relativeTime({ at: Number(task.updated_at) })}
</Span>
) : null}
</Row>
{task.latest_note ? (
<Span className="text-xs leading-4 text-ink-secondary line-clamp-2">{task.latest_note}</Span>
) : null}
{can_manage && onStatusChange ? (
<TaskStatusMoves status={task.status} disabled={busy} onMove={onStatusChange} className="pt-0.5" />
) : null}
</Stack>
</Row>
);
}