mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
Merge pull request #416 from AutoMaker-Org/feat/emtpy-columns-enhancments
feat: add empty state card component and integrate AI suggestion func…
This commit is contained in:
@@ -1289,6 +1289,8 @@ export function BoardView() {
|
|||||||
selectedFeatureIds={selectedFeatureIds}
|
selectedFeatureIds={selectedFeatureIds}
|
||||||
onToggleFeatureSelection={toggleFeatureSelection}
|
onToggleFeatureSelection={toggleFeatureSelection}
|
||||||
onToggleSelectionMode={toggleSelectionMode}
|
onToggleSelectionMode={toggleSelectionMode}
|
||||||
|
isDragging={activeFeature !== null}
|
||||||
|
onAiSuggest={() => setShowPlanDialog(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Kbd } from '@/components/ui/kbd';
|
||||||
|
import { formatShortcut } from '@/store/app-store';
|
||||||
|
import { getEmptyStateConfig, type EmptyStateConfig } from '../constants';
|
||||||
|
import { Lightbulb, Play, Clock, CheckCircle2, Sparkles, Wand2 } from 'lucide-react';
|
||||||
|
|
||||||
|
const ICON_MAP = {
|
||||||
|
lightbulb: Lightbulb,
|
||||||
|
play: Play,
|
||||||
|
clock: Clock,
|
||||||
|
check: CheckCircle2,
|
||||||
|
sparkles: Sparkles,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface EmptyStateCardProps {
|
||||||
|
columnId: string;
|
||||||
|
columnTitle?: string;
|
||||||
|
/** Keyboard shortcut for adding features (from settings) */
|
||||||
|
addFeatureShortcut?: string;
|
||||||
|
/** Whether the column is empty due to active filters */
|
||||||
|
isFilteredEmpty?: boolean;
|
||||||
|
/** Whether we're in read-only mode (hide actions) */
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
/** Called when user clicks "Use AI Suggestions" */
|
||||||
|
onAiSuggest?: () => void;
|
||||||
|
/** Card opacity (matches board settings) */
|
||||||
|
opacity?: number;
|
||||||
|
/** Enable glassmorphism effect */
|
||||||
|
glassmorphism?: boolean;
|
||||||
|
/** Custom config override for pipeline steps */
|
||||||
|
customConfig?: Partial<EmptyStateConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptyStateCard = memo(function EmptyStateCard({
|
||||||
|
columnId,
|
||||||
|
addFeatureShortcut,
|
||||||
|
isFilteredEmpty = false,
|
||||||
|
isReadOnly = false,
|
||||||
|
onAiSuggest,
|
||||||
|
customConfig,
|
||||||
|
}: EmptyStateCardProps) {
|
||||||
|
// Get base config and merge with custom overrides
|
||||||
|
const baseConfig = getEmptyStateConfig(columnId);
|
||||||
|
const config: EmptyStateConfig = { ...baseConfig, ...customConfig };
|
||||||
|
|
||||||
|
const IconComponent = ICON_MAP[config.icon];
|
||||||
|
const showActions = !isReadOnly && !isFilteredEmpty;
|
||||||
|
const showShortcut = columnId === 'backlog' && addFeatureShortcut && showActions;
|
||||||
|
|
||||||
|
// Action button handler
|
||||||
|
const handlePrimaryAction = () => {
|
||||||
|
if (!config.primaryAction) return;
|
||||||
|
if (config.primaryAction.actionType === 'ai-suggest') {
|
||||||
|
onAiSuggest?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full h-full min-h-[200px] flex-1',
|
||||||
|
'flex flex-col items-center justify-center',
|
||||||
|
'text-center px-4',
|
||||||
|
'transition-all duration-300 ease-out',
|
||||||
|
'animate-in fade-in duration-300',
|
||||||
|
'group'
|
||||||
|
)}
|
||||||
|
data-testid={`empty-state-card-${columnId}`}
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="mb-3 text-muted-foreground/30">
|
||||||
|
<IconComponent className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h4 className="font-medium text-sm text-muted-foreground/50 mb-1">
|
||||||
|
{isFilteredEmpty ? 'No Matching Items' : config.title}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-xs text-muted-foreground/40 leading-relaxed max-w-[180px]">
|
||||||
|
{isFilteredEmpty ? 'No features match your current filters.' : config.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Keyboard shortcut hint for backlog */}
|
||||||
|
{showShortcut && (
|
||||||
|
<div className="flex items-center gap-1.5 mt-3 text-muted-foreground/40">
|
||||||
|
<span className="text-xs">Press</span>
|
||||||
|
<Kbd className="bg-muted/30 border-0 px-1.5 py-0.5 text-[10px] text-muted-foreground/50">
|
||||||
|
{formatShortcut(addFeatureShortcut, true)}
|
||||||
|
</Kbd>
|
||||||
|
<span className="text-xs">to add</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI Suggest action for backlog */}
|
||||||
|
{showActions && config.primaryAction && config.primaryAction.actionType === 'ai-suggest' && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="mt-4 h-7 text-xs text-muted-foreground/50 hover:text-muted-foreground/70"
|
||||||
|
onClick={handlePrimaryAction}
|
||||||
|
data-testid={`empty-state-primary-action-${columnId}`}
|
||||||
|
>
|
||||||
|
<Wand2 className="w-3 h-3 mr-1.5" />
|
||||||
|
{config.primaryAction.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filtered empty state hint */}
|
||||||
|
{isFilteredEmpty && (
|
||||||
|
<p className="text-[10px] mt-2 text-muted-foreground/30 italic">
|
||||||
|
Clear filters to see all items
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export { KanbanCard } from './kanban-card/kanban-card';
|
export { KanbanCard } from './kanban-card/kanban-card';
|
||||||
export { KanbanColumn } from './kanban-column';
|
export { KanbanColumn } from './kanban-column';
|
||||||
export { SelectionActionBar } from './selection-action-bar';
|
export { SelectionActionBar } from './selection-action-bar';
|
||||||
|
export { EmptyStateCard } from './empty-state-card';
|
||||||
|
|||||||
@@ -3,6 +3,69 @@ import type { PipelineConfig, FeatureStatusWithPipeline } from '@automaker/types
|
|||||||
|
|
||||||
export type ColumnId = Feature['status'];
|
export type ColumnId = Feature['status'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty state configuration for each column type
|
||||||
|
*/
|
||||||
|
export interface EmptyStateConfig {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: 'lightbulb' | 'play' | 'clock' | 'check' | 'sparkles';
|
||||||
|
shortcutKey?: string; // Keyboard shortcut label (e.g., 'N', 'A')
|
||||||
|
shortcutHint?: string; // Human-readable shortcut hint
|
||||||
|
primaryAction?: {
|
||||||
|
label: string;
|
||||||
|
actionType: 'ai-suggest' | 'none';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default empty state configurations per column type
|
||||||
|
*/
|
||||||
|
export const EMPTY_STATE_CONFIGS: Record<string, EmptyStateConfig> = {
|
||||||
|
backlog: {
|
||||||
|
title: 'Ready for Ideas',
|
||||||
|
description:
|
||||||
|
'Add your first feature idea to get started using the button below, or let AI help generate ideas.',
|
||||||
|
icon: 'lightbulb',
|
||||||
|
shortcutHint: 'Press',
|
||||||
|
primaryAction: {
|
||||||
|
label: 'Use AI Suggestions',
|
||||||
|
actionType: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
in_progress: {
|
||||||
|
title: 'Nothing in Progress',
|
||||||
|
description: 'Drag a feature from the backlog here or click implement to start working on it.',
|
||||||
|
icon: 'play',
|
||||||
|
},
|
||||||
|
waiting_approval: {
|
||||||
|
title: 'No Items Awaiting Approval',
|
||||||
|
description: 'Features will appear here after implementation is complete and need your review.',
|
||||||
|
icon: 'clock',
|
||||||
|
},
|
||||||
|
verified: {
|
||||||
|
title: 'No Verified Features',
|
||||||
|
description: 'Approved features will appear here. They can then be completed and archived.',
|
||||||
|
icon: 'check',
|
||||||
|
},
|
||||||
|
// Pipeline step default configuration
|
||||||
|
pipeline_default: {
|
||||||
|
title: 'Pipeline Step Empty',
|
||||||
|
description: 'Features will flow through this step during the automated pipeline process.',
|
||||||
|
icon: 'sparkles',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get empty state config for a column, with fallback for pipeline columns
|
||||||
|
*/
|
||||||
|
export function getEmptyStateConfig(columnId: string): EmptyStateConfig {
|
||||||
|
if (columnId.startsWith('pipeline_')) {
|
||||||
|
return EMPTY_STATE_CONFIGS.pipeline_default;
|
||||||
|
}
|
||||||
|
return EMPTY_STATE_CONFIGS[columnId] || EMPTY_STATE_CONFIGS.default;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Column {
|
export interface Column {
|
||||||
id: FeatureStatusWithPipeline;
|
id: FeatureStatusWithPipeline;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { DndContext, DragOverlay } from '@dnd-kit/core';
|
import { DndContext, DragOverlay } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { KanbanColumn, KanbanCard } from './components';
|
import { KanbanColumn, KanbanCard, EmptyStateCard } from './components';
|
||||||
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
|
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
|
||||||
import { Archive, Settings2, CheckSquare, GripVertical, Plus } from 'lucide-react';
|
import { Archive, Settings2, CheckSquare, GripVertical, Plus } from 'lucide-react';
|
||||||
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
||||||
@@ -51,6 +51,12 @@ interface KanbanBoardProps {
|
|||||||
selectedFeatureIds?: Set<string>;
|
selectedFeatureIds?: Set<string>;
|
||||||
onToggleFeatureSelection?: (featureId: string) => void;
|
onToggleFeatureSelection?: (featureId: string) => void;
|
||||||
onToggleSelectionMode?: () => void;
|
onToggleSelectionMode?: () => void;
|
||||||
|
// Empty state action props
|
||||||
|
onAiSuggest?: () => void;
|
||||||
|
/** Whether currently dragging (hides empty states during drag) */
|
||||||
|
isDragging?: boolean;
|
||||||
|
/** Whether the board is in read-only mode */
|
||||||
|
isReadOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KanbanBoard({
|
export function KanbanBoard({
|
||||||
@@ -86,6 +92,9 @@ export function KanbanBoard({
|
|||||||
selectedFeatureIds = new Set(),
|
selectedFeatureIds = new Set(),
|
||||||
onToggleFeatureSelection,
|
onToggleFeatureSelection,
|
||||||
onToggleSelectionMode,
|
onToggleSelectionMode,
|
||||||
|
onAiSuggest,
|
||||||
|
isDragging = false,
|
||||||
|
isReadOnly = false,
|
||||||
}: KanbanBoardProps) {
|
}: KanbanBoardProps) {
|
||||||
// Generate columns including pipeline steps
|
// Generate columns including pipeline steps
|
||||||
const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]);
|
const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]);
|
||||||
@@ -211,6 +220,26 @@ export function KanbanBoard({
|
|||||||
items={columnFeatures.map((f) => f.id)}
|
items={columnFeatures.map((f) => f.id)}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
|
{/* Empty state card when column has no features */}
|
||||||
|
{columnFeatures.length === 0 && !isDragging && (
|
||||||
|
<EmptyStateCard
|
||||||
|
columnId={column.id}
|
||||||
|
columnTitle={column.title}
|
||||||
|
addFeatureShortcut={addFeatureShortcut}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
onAiSuggest={column.id === 'backlog' ? onAiSuggest : undefined}
|
||||||
|
opacity={backgroundSettings.cardOpacity}
|
||||||
|
glassmorphism={backgroundSettings.cardGlassmorphism}
|
||||||
|
customConfig={
|
||||||
|
column.isPipelineStep
|
||||||
|
? {
|
||||||
|
title: `${column.title} Empty`,
|
||||||
|
description: `Features will appear here during the ${column.title.toLowerCase()} phase of the pipeline.`,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{columnFeatures.map((feature, index) => {
|
{columnFeatures.map((feature, index) => {
|
||||||
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
|
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
|
||||||
let shortcutKey: string | undefined;
|
let shortcutKey: string | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user