feat(plugins): add source-control extension contracts (#2117)
This commit is contained in:
@@ -1,151 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "@/lib/routing/client-router";
|
||||
import { IconChecklist } from "@tabler/icons-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@kandev/ui/dropdown-menu";
|
||||
import { Badge } from "@kandev/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { linkToTask } from "@/lib/links";
|
||||
import { useTaskById } from "@/hooks/domains/kanban/use-task-by-id";
|
||||
import type { KanbanState } from "@/lib/state/slices";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type TaskRowLink = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
fallbackTitle: string;
|
||||
};
|
||||
|
||||
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",
|
||||
);
|
||||
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 taskData = useTaskById(task.taskId);
|
||||
const stepTitle = useTaskStepTitle(taskData?.workflowStepId);
|
||||
const title = taskData?.title ?? task.fallbackTitle;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`${testIdPrefix}-single`}
|
||||
onClick={() => onNavigate(task.taskId)}
|
||||
className={buttonClass}
|
||||
>
|
||||
<IconChecklist className={iconClass} />
|
||||
<span className="truncate text-foreground/80">{title}</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 px-1 py-0 text-[10px] shrink-0">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
export {
|
||||
TaskRowIndicator,
|
||||
type TaskRowIndicatorProps,
|
||||
type TaskRowLink,
|
||||
} from "@/components/integrations/task-row-indicator";
|
||||
|
||||
Reference in New Issue
Block a user