"use client"; import { memo, useMemo, useRef } from "react"; import { cn } from "@kandev/ui/lib/utils"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@kandev/ui/hover-card"; import { Button } from "@kandev/ui/button"; import { IconArrowRight } from "@tabler/icons-react"; import { StepCapabilityIcons } from "@/components/step-capability-icons"; import { useToolbarCollapsed } from "@/hooks/use-toolbar-collapsed"; import { usePresentationToken, useWorkflowStepMove, } from "@/hooks/domains/kanban/use-workflow-step-move"; import { sortWorkflowStepsByPosition } from "@/lib/kanban/workflow-step-order"; import { useTranslation } from "react-i18next"; import { MinimalWorkflowStepper, StepCircleIndicator, canMoveToStep, getStepLabelClass, type WorkflowStepperStep, } from "./workflow-step-disclosure"; type Step = WorkflowStepperStep; type WorkflowStepperProps = { steps: Step[]; currentStepId: string | null; taskId?: string | null; workflowId?: string | null; isArchived?: boolean; onMoveStart?: () => void; onMoveError?: (error: unknown) => void; }; const WorkflowStepper = memo(function WorkflowStepper({ steps, currentStepId, taskId, workflowId, isArchived, onMoveStart, onMoveError, }: WorkflowStepperProps) { const { t } = useTranslation(); // The task route's continuous presentation of this task: a navigation away // and back changes taskId and back, which must invalidate a request left // over from the earlier presentation the same way a preview close-and-reopen // does for the kanban preview header. const presentationToken = usePresentationToken(taskId ?? null); const { movingToStepId, handleMove } = useWorkflowStepMove({ taskId, workflowId, presentationToken, onMoveStart, onMoveError, }); const sortedSteps = useMemo(() => sortWorkflowStepsByPosition(steps), [steps]); const currentIndex = useMemo( () => sortedSteps.findIndex((s) => s.id === currentStepId), [sortedSteps, currentStepId], ); // Collapse to a minimal view when the full stepper can't fit (w-full keeps the measurement track-driven). const containerRef = useRef(null); const isCollapsed = useToolbarCollapsed(containerRef); if (sortedSteps.length === 0) return null; return (
{isCollapsed ? ( ) : ( <>
{sortedSteps.map((step, index) => ( ))}
{isArchived && ( <>
{t("task:filterDimensionArchived")} )} )}
); }); /** Individual step in the workflow stepper */ function WorkflowStepItem({ step, index, currentIndex, isArchived, taskId, workflowId, movingToStepId, onMove, }: { step: Step; index: number; currentIndex: number; isArchived?: boolean; taskId?: string | null; workflowId?: string | null; movingToStepId: string | null; onMove: (stepId: string) => void; }) { const isCompleted = !isArchived && currentIndex >= 0 && index < currentIndex; const isCurrent = !isArchived && index === currentIndex; const isAdjacent = currentIndex >= 0 && (index === currentIndex - 1 || index === currentIndex + 1); const canMove = canMoveToStep({ isArchived, isCurrent, taskId, workflowId, isAdjacent, allowManualMove: step.allow_manual_move, }); return (
{index > 0 && }
{step.name}
); } /** Connector line between steps */ function StepConnector({ isActive }: { isActive: boolean }) { return (
); } /** Hover content for a workflow step */ function StepHoverContent({ step, isCurrent, canMove, isMoving, onMove, }: { step: Step; isCurrent: boolean; canMove: boolean; isMoving: boolean; onMove: (stepId: string) => void; }) { const { t } = useTranslation(); return ( {canMove && ( )} {isCurrent && (
{t("task:currentStep")}
)}
); } export { WorkflowStepper }; export type { WorkflowStepperStep } from "./workflow-step-disclosure";