import { useState, useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { GripVertical, Plus, Pencil, Trash2, FileText, Lock, MoreHorizontal } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { cn } from '@/lib/utils'; import type { FeatureTemplate, PhaseModelEntry } from '@automaker/types'; import { PhaseModelSelector } from '../model-defaults/phase-model-selector'; import { useAppStore } from '@/store/app-store'; import { toast } from 'sonner'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; interface TemplatesSectionProps { templates: FeatureTemplate[]; onAddTemplate: (template: FeatureTemplate) => Promise; onUpdateTemplate: (id: string, updates: Partial) => Promise; onDeleteTemplate: (id: string) => Promise; onReorderTemplates: (templateIds: string[]) => Promise; } interface TemplateFormData { name: string; prompt: string; model?: PhaseModelEntry; } const MAX_NAME_LENGTH = 50; function generateId(): string { return `template-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } function SortableTemplateItem({ template, onEdit, onToggleEnabled, onDelete, }: { template: FeatureTemplate; onEdit: () => void; onToggleEnabled: () => void; onDelete: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: template.id, }); const style = { transform: CSS.Transform.toString(transform), transition, }; const isEnabled = template.enabled !== false; return (
{/* Drag Handle */} {/* Template Info */}
{template.name} {template.isBuiltIn && ( )} {!isEnabled && ( Disabled )}

{template.prompt}

{/* Actions */} Edit {isEnabled ? 'Disable' : 'Enable'} {!template.isBuiltIn && ( Delete )}
); } export function TemplatesSection({ templates, onAddTemplate, onUpdateTemplate, onDeleteTemplate, onReorderTemplates, }: TemplatesSectionProps) { const [dialogOpen, setDialogOpen] = useState(false); const [editingTemplate, setEditingTemplate] = useState(null); const [formData, setFormData] = useState({ name: '', prompt: '', }); const [nameError, setNameError] = useState(false); const [promptError, setPromptError] = useState(false); const defaultFeatureModel = useAppStore((s) => s.defaultFeatureModel); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const handleAddNew = () => { setEditingTemplate(null); setFormData({ name: '', prompt: '', model: undefined, }); setNameError(false); setPromptError(false); setDialogOpen(true); }; const handleEdit = (template: FeatureTemplate) => { setEditingTemplate(template); setFormData({ name: template.name, prompt: template.prompt, model: template.model, }); setNameError(false); setPromptError(false); setDialogOpen(true); }; const handleToggleEnabled = async (template: FeatureTemplate) => { await onUpdateTemplate(template.id, { enabled: template.enabled === false ? true : false }); }; const handleDelete = async (template: FeatureTemplate) => { if (template.isBuiltIn) { toast.error('Built-in templates cannot be deleted'); return; } await onDeleteTemplate(template.id); toast.success('Template deleted'); }; const handleSave = async () => { // Validate let hasError = false; if (!formData.name.trim()) { setNameError(true); hasError = true; } if (!formData.prompt.trim()) { setPromptError(true); hasError = true; } if (hasError) return; if (editingTemplate) { // Update existing await onUpdateTemplate(editingTemplate.id, { name: formData.name.trim(), prompt: formData.prompt.trim(), model: formData.model, }); toast.success('Template updated'); } else { // Create new const newTemplate: FeatureTemplate = { id: generateId(), name: formData.name.trim(), prompt: formData.prompt.trim(), model: formData.model, isBuiltIn: false, enabled: true, order: Math.max(...templates.map((t) => t.order ?? 0), -1) + 1, }; await onAddTemplate(newTemplate); toast.success('Template created'); } setDialogOpen(false); }; // Memoized sorted copy — avoids mutating the Zustand-managed templates array const sortedTemplates = useMemo( () => [...templates].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), [templates] ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = sortedTemplates.findIndex((t) => t.id === active.id); const newIndex = sortedTemplates.findIndex((t) => t.id === over.id); const reordered = arrayMove(sortedTemplates, oldIndex, newIndex); onReorderTemplates(reordered.map((t) => t.id)); } }; return (

Feature Templates

Create reusable task templates for quick feature creation from the Add Feature dropdown.

{templates.length === 0 ? (

No templates yet

Create your first template to get started

) : ( t.id)} strategy={verticalListSortingStrategy} >
{sortedTemplates.map((template) => ( handleEdit(template)} onToggleEnabled={() => handleToggleEnabled(template)} onDelete={() => handleDelete(template)} /> ))}
)}
{/* Add/Edit Dialog */} {editingTemplate ? 'Edit Template' : 'Create Template'} {editingTemplate ? 'Update the template details below.' : 'Create a new template for quick feature creation.'}
{/* Name */}
{ setFormData({ ...formData, name: e.target.value }); if (e.target.value.trim()) setNameError(false); }} placeholder="e.g., Run tests and fix issues" maxLength={MAX_NAME_LENGTH} className={nameError ? 'border-destructive' : ''} data-testid="template-name-input" />
{nameError && Name is required} {formData.name.length}/{MAX_NAME_LENGTH}
{/* Prompt */}