'use client'; import { useState, useMemo } from 'react'; import { cn } from '@/lib/utils/cn'; import { parseField, rebuildFieldFromValues, validateCronField, FIELD_CONFIGS, MONTH_SHORT_NAMES, DOW_SHORT_NAMES, type FieldType, } from '@/lib/cron/cron-engine'; // ── Per-field presets ───────────────────────────────────────────────────────── interface Preset { label: string; value: string } const FIELD_PRESETS: Record = { second: [ { label: 'Any (*)', value: '*' }, { label: '*/5', value: '*/5' }, { label: '*/10', value: '*/10' }, { label: '*/15', value: '*/15' }, { label: '*/30', value: '*/30' }, ], minute: [ { label: 'Any (*)', value: '*' }, { label: ':00', value: '0' }, { label: ':30', value: '30' }, { label: '*/5', value: '*/5' }, { label: '*/10', value: '*/10' }, { label: '*/15', value: '*/15' }, { label: '*/30', value: '*/30' }, ], hour: [ { label: 'Any (*)', value: '*' }, { label: 'Midnight', value: '0' }, { label: '6 AM', value: '6' }, { label: '9 AM', value: '9' }, { label: 'Noon', value: '12' }, { label: '6 PM', value: '18' }, { label: 'Every 4h', value: '*/4' }, { label: 'Every 6h', value: '*/6' }, { label: '9–17', value: '9-17' }, ], dom: [ { label: 'Any (*)', value: '*' }, { label: '1st', value: '1' }, { label: '10th', value: '10' }, { label: '15th', value: '15' }, { label: '20th', value: '20' }, { label: '1,15', value: '1,15' }, { label: '1–7', value: '1-7' }, ], month: [ { label: 'Any (*)', value: '*' }, { label: 'Q1', value: '1-3' }, { label: 'Q2', value: '4-6' }, { label: 'Q3', value: '7-9' }, { label: 'Q4', value: '10-12' }, { label: 'H1', value: '1-6' }, { label: 'H2', value: '7-12' }, ], dow: [ { label: 'Any (*)', value: '*' }, { label: 'Weekdays', value: '1-5' }, { label: 'Weekends', value: '0,6' }, { label: 'Mon', value: '1' }, { label: 'Wed', value: '3' }, { label: 'Fri', value: '5' }, { label: 'Sun', value: '0' }, ], }; // ── Grid configuration ──────────────────────────────────────────────────────── const GRID_COLS: Record = { second: 'grid-cols-10', minute: 'grid-cols-10', hour: 'grid-cols-8', dom: 'grid-cols-7', month: 'grid-cols-4', dow: 'grid-cols-7', }; // ── Component ───────────────────────────────────────────────────────────────── interface CronFieldEditorProps { fieldType: FieldType; value: string; onChange: (value: string) => void; } export function CronFieldEditor({ fieldType, value, onChange }: CronFieldEditorProps) { const [rawInput, setRawInput] = useState(''); const [showRaw, setShowRaw] = useState(false); const [rawError, setRawError] = useState(''); const config = FIELD_CONFIGS[fieldType]; const parsed = useMemo(() => parseField(value, config), [value, config]); const presets = FIELD_PRESETS[fieldType]; const isWildcard = parsed?.isWildcard ?? false; const isSelected = (v: number) => parsed?.values.has(v) ?? false; const cellLabel = (v: number): string => { if (fieldType === 'month') return MONTH_SHORT_NAMES[v - 1]; if (fieldType === 'dow') return DOW_SHORT_NAMES[v]; return String(v).padStart(fieldType === 'second' || fieldType === 'minute' ? 2 : 1, '0'); }; const handleCellClick = (v: number) => { if (!parsed) return; if (isWildcard) { onChange(String(v)); return; } const next = new Set(parsed.values); if (next.has(v)) { next.delete(v); if (next.size === 0) { onChange('*'); return; } } else { next.add(v); if (next.size === config.max - config.min + 1) { onChange('*'); return; } } onChange(rebuildFieldFromValues(next, config)); }; const handleRawSubmit = () => { const { valid, error } = validateCronField(rawInput, fieldType); if (valid) { onChange(rawInput); setShowRaw(false); setRawInput(''); setRawError(''); } else { setRawError(error ?? 'Invalid'); } }; const cells = Array.from({ length: config.max - config.min + 1 }, (_, i) => i + config.min); // Pad to complete rows for DOM (31 cells, 7 cols → pad to 35) const colCount = parseInt(GRID_COLS[fieldType].replace('grid-cols-', ''), 10); const rem = cells.length % colCount; const padded: (number | null)[] = [...cells, ...(rem === 0 ? [] : Array(colCount - rem).fill(null))]; return (
{/* Header */}
{config.label} {config.min}–{config.max}
{isWildcard && ( any value )} {value}
{/* Presets */}
{presets.map((preset) => ( ))}
{/* Value grid */}
{padded.map((v, i) => { if (v === null) return
; const selected = isSelected(v); return ( ); })}
{/* Custom raw input */}
{showRaw ? (
{ setRawInput(e.target.value); setRawError(''); }} onKeyDown={(e) => { if (e.key === 'Enter') handleRawSubmit(); if (e.key === 'Escape') { setShowRaw(false); setRawError(''); } }} placeholder={`e.g. ${fieldType === 'minute' ? '*/15 or 0,30' : fieldType === 'hour' ? '9-17' : fieldType === 'dow' ? '1-5' : '*'}`} className={cn( 'flex-1 px-3 py-1.5 text-xs font-mono bg-muted/20 border rounded-lg focus:outline-none transition-colors', rawError ? 'border-destructive/50 focus:border-destructive' : 'border-border/30 focus:border-primary/50', )} autoFocus />
{rawError && (

{rawError}

)}
) : ( )}
); }