mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
275 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|