import { useState, useEffect } 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Terminal, Globe, Bell } from 'lucide-react'; import type { EventHook, EventHookTrigger, EventHookHttpMethod, EventHookShellAction, EventHookHttpAction, EventHookNtfyAction, } from '@automaker/types'; import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types'; import { generateUUID } from '@/lib/utils'; import { useAppStore } from '@/store/app-store'; import { toast } from 'sonner'; interface EventHookDialogProps { open: boolean; onOpenChange: (open: boolean) => void; editingHook: EventHook | null; onSave: (hook: EventHook) => void; } type ActionType = 'shell' | 'http' | 'ntfy'; const TRIGGER_OPTIONS: EventHookTrigger[] = [ 'feature_created', 'feature_success', 'feature_error', 'auto_mode_complete', 'auto_mode_error', ]; const HTTP_METHODS: EventHookHttpMethod[] = ['POST', 'GET', 'PUT', 'PATCH']; const PRIORITY_OPTIONS = [ { value: 1, label: 'Min (no sound/vibration)' }, { value: 2, label: 'Low' }, { value: 3, label: 'Default' }, { value: 4, label: 'High' }, { value: 5, label: 'Urgent (max)' }, ]; export function EventHookDialog({ open, onOpenChange, editingHook, onSave }: EventHookDialogProps) { const ntfyEndpoints = useAppStore((state) => state.ntfyEndpoints); // Form state const [name, setName] = useState(''); const [trigger, setTrigger] = useState('feature_success'); const [actionType, setActionType] = useState('shell'); // Shell action state const [command, setCommand] = useState(''); const [timeout, setTimeout] = useState('30000'); // HTTP action state const [url, setUrl] = useState(''); const [method, setMethod] = useState('POST'); const [headers, setHeaders] = useState(''); const [body, setBody] = useState(''); // Ntfy action state const [ntfyEndpointId, setNtfyEndpointId] = useState(''); const [ntfyTitle, setNtfyTitle] = useState(''); const [ntfyBody, setNtfyBody] = useState(''); const [ntfyTags, setNtfyTags] = useState(''); const [ntfyEmoji, setNtfyEmoji] = useState(''); const [ntfyClickUrl, setNtfyClickUrl] = useState(''); const [ntfyPriority, setNtfyPriority] = useState<1 | 2 | 3 | 4 | 5>(3); // Reset form when dialog opens/closes or editingHook changes useEffect(() => { if (open) { if (editingHook) { // Populate form with existing hook data setName(editingHook.name || ''); setTrigger(editingHook.trigger); setActionType(editingHook.action.type as ActionType); if (editingHook.action.type === 'shell') { const shellAction = editingHook.action as EventHookShellAction; setCommand(shellAction.command); setTimeout(String(shellAction.timeout || 30000)); // Reset other fields resetHttpFields(); resetNtfyFields(); } else if (editingHook.action.type === 'http') { const httpAction = editingHook.action as EventHookHttpAction; setUrl(httpAction.url); setMethod(httpAction.method); setHeaders(httpAction.headers ? JSON.stringify(httpAction.headers, null, 2) : ''); setBody(httpAction.body || ''); // Reset other fields resetShellFields(); resetNtfyFields(); } else if (editingHook.action.type === 'ntfy') { const ntfyAction = editingHook.action as EventHookNtfyAction; setNtfyEndpointId(ntfyAction.endpointId); setNtfyTitle(ntfyAction.title || ''); setNtfyBody(ntfyAction.body || ''); setNtfyTags(ntfyAction.tags || ''); setNtfyEmoji(ntfyAction.emoji || ''); setNtfyClickUrl(ntfyAction.clickUrl || ''); setNtfyPriority(ntfyAction.priority || 3); // Reset other fields resetShellFields(); resetHttpFields(); } } else { // Reset to defaults for new hook setName(''); setTrigger('feature_success'); setActionType('shell'); resetShellFields(); resetHttpFields(); resetNtfyFields(); } } }, [open, editingHook]); const resetShellFields = () => { setCommand(''); setTimeout('30000'); }; const resetHttpFields = () => { setUrl(''); setMethod('POST'); setHeaders(''); setBody(''); }; const resetNtfyFields = () => { setNtfyEndpointId(''); setNtfyTitle(''); setNtfyBody(''); setNtfyTags(''); setNtfyEmoji(''); setNtfyClickUrl(''); setNtfyPriority(3); }; const handleSave = () => { let action: EventHook['action']; if (actionType === 'shell') { action = { type: 'shell', command, timeout: parseInt(timeout, 10) || 30000, }; } else if (actionType === 'http') { // Parse headers JSON with error handling let parsedHeaders: Record | undefined; if (headers.trim()) { try { parsedHeaders = JSON.parse(headers); } catch { // If JSON is invalid, show error and don't save toast.error('Invalid JSON in Headers field'); return; } } action = { type: 'http', url, method, headers: parsedHeaders, body: body.trim() || undefined, }; } else { action = { type: 'ntfy', endpointId: ntfyEndpointId, title: ntfyTitle.trim() || undefined, body: ntfyBody.trim() || undefined, tags: ntfyTags.trim() || undefined, emoji: ntfyEmoji.trim() || undefined, clickUrl: ntfyClickUrl.trim() || undefined, priority: ntfyPriority, }; } const hook: EventHook = { id: editingHook?.id || generateUUID(), name: name.trim() || undefined, trigger, enabled: editingHook?.enabled ?? true, action, }; onSave(hook); }; const selectedEndpoint = ntfyEndpoints.find((e) => e.id === ntfyEndpointId); const isValid = (() => { if (actionType === 'shell') return command.trim().length > 0; if (actionType === 'http') return url.trim().length > 0; if (actionType === 'ntfy') return Boolean(selectedEndpoint); return false; })(); return ( {editingHook ? 'Edit Event Hook' : 'Add Event Hook'} Configure an action to run when a specific event occurs.
{/* Name (optional) */}
setName(e.target.value)} placeholder="My notification hook" />
{/* Trigger selection */}
{/* Action type tabs */}
setActionType(v as ActionType)}> Shell HTTP Ntfy {/* Shell command form */}