Files
2026-09-12 10:06:05 +02:00

165 lines
5.3 KiB
TypeScript

"use client";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { IconChecklist } from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@kandev/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useAppStore } from "@/components/state-provider";
import { useTaskById } from "@/hooks/domains/kanban/use-task-by-id";
import { linkToTask } from "@/lib/links";
import { useRouter } from "@/lib/routing/client-router";
import type { KanbanState } from "@/lib/state/slices";
import { cn } from "@/lib/utils";
export type TaskRowLink = {
id: string;
taskId: string;
fallbackTitle: string;
};
export type TaskRowIndicatorProps = {
tasks: TaskRowLink[] | undefined;
testIdPrefix: string;
emptyLabel?: string;
};
function useTaskStepTitle(workflowStepId: string | undefined): string | null {
return useAppStore((state) => {
if (!workflowStepId) return null;
const findIn = (steps: KanbanState["steps"]) =>
steps.find((step) => step.id === workflowStepId)?.title ?? null;
const activeTitle = findIn(state.kanban.steps);
if (activeTitle) return activeTitle;
for (const snapshot of Object.values(state.kanbanMulti.snapshots)) {
const title = findIn(snapshot.steps);
if (title) return title;
}
return null;
});
}
function TaskTitle({ task }: { task: TaskRowLink }) {
const taskData = useTaskById(task.taskId);
const title = taskData?.title ?? task.fallbackTitle;
return (
<span className="truncate text-foreground/80" title={title}>
{title}
</span>
);
}
const buttonClass = cn(
"inline-flex items-center gap-1.5 rounded px-1.5 py-0.5 text-xs",
"hover:bg-muted/70 transition-colors cursor-pointer w-fit max-w-full",
"min-h-11 min-w-11 text-left md:min-h-0 md:min-w-0 [@media(pointer:coarse)]:min-h-11 [@media(pointer:coarse)]:min-w-11 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
);
const iconClass = "h-3 w-3 shrink-0 text-muted-foreground";
function SingleTaskButton({
task,
testIdPrefix,
onNavigate,
}: {
task: TaskRowLink;
testIdPrefix: string;
onNavigate: (taskId: string) => void;
}) {
const { t } = useTranslation();
const taskData = useTaskById(task.taskId);
const stepTitle = useTaskStepTitle(taskData?.workflowStepId);
const title = taskData?.title ?? task.fallbackTitle;
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
data-testid={`${testIdPrefix}-single`}
onClick={() => onNavigate(task.taskId)}
className={buttonClass}
>
<IconChecklist className={iconClass} />
<span className="min-w-0">
<span className="block wrap-anywhere text-foreground/80 md:truncate [@media(pointer:coarse)]:whitespace-normal">
{title}
</span>
{stepTitle && (
<span
data-testid="task-row-step-title"
className="block wrap-anywhere text-muted-foreground md:hidden [@media(pointer:coarse)]:block"
>
{t("github:stepLabelled", { stepTitle })}
</span>
)}
</span>
</button>
</TooltipTrigger>
<TooltipContent>
<div className="flex flex-col gap-0.5 text-xs">
<span>{t("github:taskLabelled", { title })}</span>
{stepTitle ? (
<span className="text-muted-foreground">{t("github:stepLabelled", { stepTitle })}</span>
) : null}
</div>
</TooltipContent>
</Tooltip>
);
}
export function TaskRowIndicator({ tasks, testIdPrefix, emptyLabel }: TaskRowIndicatorProps) {
const { t } = useTranslation();
const setActiveTask = useAppStore((state) => state.setActiveTask);
const router = useRouter();
const navigate = useCallback(
(taskId: string) => {
setActiveTask(taskId);
router.push(linkToTask(taskId));
},
[router, setActiveTask],
);
if (!tasks || tasks.length === 0) {
return emptyLabel ? (
<span
data-testid={`${testIdPrefix}-empty`}
className="inline-flex items-center gap-1 text-xs text-muted-foreground"
>
{emptyLabel}
</span>
) : null;
}
if (tasks.length === 1) {
return <SingleTaskButton task={tasks[0]} testIdPrefix={testIdPrefix} onNavigate={navigate} />;
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button type="button" data-testid={`${testIdPrefix}-multi`} className={buttonClass}>
<IconChecklist className={iconClass} />
<span className="text-foreground/80">{t("github:tasks")}</span>
<Badge variant="outline" className="h-4 shrink-0 px-1 py-0 text-[10px]">
{tasks.length}
</Badge>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
{tasks.map((task) => (
<DropdownMenuItem
key={task.id}
className="cursor-pointer"
onSelect={() => navigate(task.taskId)}
>
<TaskTitle task={task} />
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}