Files
automaker/apps/ui/src/components/views/board-view/kanban-board.tsx

275 lines
11 KiB
TypeScript

import { useMemo } from 'react';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { Button } from '@/components/ui/button';
import { HotkeyButton } from '@/components/ui/hotkey-button';
import { KanbanColumn, KanbanCard } from './components';
import { Feature } from '@/store/app-store';
import { FastForward, Lightbulb, Archive, Plus, Settings2 } from 'lucide-react';
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
import { getColumnsWithPipeline, type Column, type ColumnId } from './constants';
import type { PipelineConfig } from '@automaker/types';
interface KanbanBoardProps {
sensors: any;
collisionDetectionStrategy: (args: any) => any;
onDragStart: (event: any) => void;
onDragEnd: (event: any) => void;
activeFeature: Feature | null;
getColumnFeatures: (columnId: ColumnId) => Feature[];
backgroundImageStyle: React.CSSProperties;
backgroundSettings: {
columnOpacity: number;
columnBorderEnabled: boolean;
hideScrollbar: boolean;
cardOpacity: number;
cardGlassmorphism: boolean;
cardBorderEnabled: boolean;
cardBorderOpacity: number;
};
onEdit: (feature: Feature) => void;
onDelete: (featureId: string) => void;
onViewOutput: (feature: Feature) => void;
onVerify: (feature: Feature) => void;
onResume: (feature: Feature) => void;
onForceStop: (feature: Feature) => void;
onManualVerify: (feature: Feature) => void;
onMoveBackToInProgress: (feature: Feature) => void;
onFollowUp: (feature: Feature) => void;
onCommit: (feature: Feature) => void;
onComplete: (feature: Feature) => void;
onImplement: (feature: Feature) => void;
onViewPlan: (feature: Feature) => void;
onApprovePlan: (feature: Feature) => void;
onSpawnTask?: (feature: Feature) => void;
featuresWithContext: Set<string>;
runningAutoTasks: string[];
shortcuts: ReturnType<typeof useKeyboardShortcutsConfig>;
onStartNextFeatures: () => void;
onShowSuggestions: () => void;
suggestionsCount: number;
onArchiveAllVerified: () => void;
pipelineConfig: PipelineConfig | null;
onOpenPipelineSettings?: () => void;
}
export function KanbanBoard({
sensors,
collisionDetectionStrategy,
onDragStart,
onDragEnd,
activeFeature,
getColumnFeatures,
backgroundImageStyle,
backgroundSettings,
onEdit,
onDelete,
onViewOutput,
onVerify,
onResume,
onForceStop,
onManualVerify,
onMoveBackToInProgress,
onFollowUp,
onCommit,
onComplete,
onImplement,
onViewPlan,
onApprovePlan,
onSpawnTask,
featuresWithContext,
runningAutoTasks,
shortcuts,
onStartNextFeatures,
onShowSuggestions,
suggestionsCount,
onArchiveAllVerified,
pipelineConfig,
onOpenPipelineSettings,
}: KanbanBoardProps) {
// Generate columns including pipeline steps
const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]);
// Use responsive column widths based on window size
// containerStyle handles centering and ensures columns fit without horizontal scroll in Electron
const { columnWidth, containerStyle } = useResponsiveKanban(columns.length);
return (
<div className="flex-1 overflow-x-auto px-5 pb-4 relative" style={backgroundImageStyle}>
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<div className="h-full py-1" style={containerStyle}>
{columns.map((column) => {
const columnFeatures = getColumnFeatures(column.id as ColumnId);
return (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
colorClass={column.colorClass}
count={columnFeatures.length}
width={columnWidth}
opacity={backgroundSettings.columnOpacity}
showBorder={backgroundSettings.columnBorderEnabled}
hideScrollbar={backgroundSettings.hideScrollbar}
headerAction={
column.id === 'verified' && columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={onArchiveAllVerified}
data-testid="archive-all-verified-button"
>
<Archive className="w-3 h-3 mr-1" />
Complete All
</Button>
) : column.id === 'backlog' ? (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10 relative"
onClick={onShowSuggestions}
title="Feature Suggestions"
data-testid="feature-suggestions-button"
>
<Lightbulb className="w-3.5 h-3.5" />
{suggestionsCount > 0 && (
<span
className="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-mono rounded-full bg-yellow-500 text-black flex items-center justify-center"
data-testid="suggestions-count"
>
{suggestionsCount}
</span>
)}
</Button>
{columnFeatures.length > 0 && (
<HotkeyButton
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={onStartNextFeatures}
hotkey={shortcuts.startNext}
hotkeyActive={false}
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Make
</HotkeyButton>
)}
</div>
) : column.id === 'in_progress' ? (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
onClick={onOpenPipelineSettings}
title="Pipeline Settings"
data-testid="pipeline-settings-button"
>
<Settings2 className="w-3.5 h-3.5" />
</Button>
) : column.isPipelineStep ? (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
onClick={onOpenPipelineSettings}
title="Edit Pipeline Step"
data-testid="edit-pipeline-step-button"
>
<Settings2 className="w-3.5 h-3.5" />
</Button>
) : undefined
}
>
<SortableContext
items={columnFeatures.map((f) => f.id)}
strategy={verticalListSortingStrategy}
>
{columnFeatures.map((feature, index) => {
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
let shortcutKey: string | undefined;
if (column.id === 'in_progress' && index < 10) {
shortcutKey = index === 9 ? '0' : String(index + 1);
}
return (
<KanbanCard
key={feature.id}
feature={feature}
onEdit={() => onEdit(feature)}
onDelete={() => onDelete(feature.id)}
onViewOutput={() => onViewOutput(feature)}
onVerify={() => onVerify(feature)}
onResume={() => onResume(feature)}
onForceStop={() => onForceStop(feature)}
onManualVerify={() => onManualVerify(feature)}
onMoveBackToInProgress={() => onMoveBackToInProgress(feature)}
onFollowUp={() => onFollowUp(feature)}
onComplete={() => onComplete(feature)}
onImplement={() => onImplement(feature)}
onViewPlan={() => onViewPlan(feature)}
onApprovePlan={() => onApprovePlan(feature)}
onSpawnTask={() => onSpawnTask?.(feature)}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
shortcutKey={shortcutKey}
opacity={backgroundSettings.cardOpacity}
glassmorphism={backgroundSettings.cardGlassmorphism}
cardBorderEnabled={backgroundSettings.cardBorderEnabled}
cardBorderOpacity={backgroundSettings.cardBorderOpacity}
/>
);
})}
</SortableContext>
</KanbanColumn>
);
})}
</div>
<DragOverlay
dropAnimation={{
duration: 200,
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
}}
>
{activeFeature && (
<div style={{ width: `${columnWidth}px` }}>
<KanbanCard
feature={activeFeature}
isOverlay
onEdit={() => {}}
onDelete={() => {}}
onViewOutput={() => {}}
onVerify={() => {}}
onResume={() => {}}
onForceStop={() => {}}
onManualVerify={() => {}}
onMoveBackToInProgress={() => {}}
onFollowUp={() => {}}
onImplement={() => {}}
onComplete={() => {}}
onViewPlan={() => {}}
onApprovePlan={() => {}}
onSpawnTask={() => {}}
hasContext={featuresWithContext.has(activeFeature.id)}
isCurrentAutoTask={runningAutoTasks.includes(activeFeature.id)}
opacity={backgroundSettings.cardOpacity}
glassmorphism={backgroundSettings.cardGlassmorphism}
cardBorderEnabled={backgroundSettings.cardBorderEnabled}
cardBorderOpacity={backgroundSettings.cardBorderOpacity}
/>
</div>
)}
</DragOverlay>
</DndContext>
</div>
);
}