import { useState } from "react"; import { LuPlus } from "react-icons/lu"; import { KeySequence } from "@hooks/stores"; import useKeyboardLayout from "@hooks/useKeyboardLayout"; import { Button } from "@components/Button"; import FieldLabel from "@components/FieldLabel"; import Fieldset from "@components/Fieldset"; import { InputFieldWithLabel, FieldError } from "@components/InputField"; import { MacroStepCard } from "@components/MacroStepCard"; import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP, } from "@/constants/macros"; import { m } from "@localizations/messages.js"; interface ValidationErrors { name?: string; steps?: Record< number, { keys?: string; modifiers?: string; delay?: string; } >; } interface MacroFormProps { initialData: Partial; onSubmit: (macro: Partial) => Promise; onCancel: () => void; isSubmitting?: boolean; } export function MacroForm({ initialData, onSubmit, onCancel, isSubmitting = false, }: Readonly) { const [macro, setMacro] = useState>(initialData); const [keyQueries, setKeyQueries] = useState>({}); const [errors, setErrors] = useState({}); const [errorMessage, setErrorMessage] = useState(null); const { selectedKeyboard } = useKeyboardLayout(); const showTemporaryError = (message: string) => { setErrorMessage(message); setTimeout(() => setErrorMessage(null), 3000); }; const validateForm = (): boolean => { const newErrors: ValidationErrors = {}; // Name validation if (!macro.name?.trim()) { newErrors.name = m.macro_name_required(); } else if (macro.name.trim().length > 50) { newErrors.name = m.macro_name_too_long(); } const steps = (macro.steps || []); if (steps.length) { const hasKeyOrModifier = steps.some( step => step.keys.length > 0 || step.modifiers.length > 0, ); if (!hasKeyOrModifier) { newErrors.steps = { 0: { keys: m.macro_at_least_one_step_keys_or_modifiers() }, }; } } else { newErrors.steps = { 0: { keys: m.macro_at_least_one_step_required() } }; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async () => { if (!validateForm()) { showTemporaryError(m.macro_please_fix_validation_errors()); return; } try { await onSubmit(macro); } catch (error) { if (error instanceof Error) { showTemporaryError(m.macro_save_failed_error({error: error.message || m.unknown_error()})); } else { showTemporaryError(m.macro_save_failed()); } } }; const handleKeySelect = ( stepIndex: number, option: { value: string | null; keys?: string[] }, ) => { const newSteps = [...(macro.steps || [])]; if (!newSteps[stepIndex]) return; if (option.keys) { // they gave us a full set of keys (e.g. from deleting one) newSteps[stepIndex].keys = option.keys; } else if (option.value) { // they gave us a single key to add if (!newSteps[stepIndex].keys) { newSteps[stepIndex].keys = []; } const keysArray = newSteps[stepIndex].keys; if (keysArray.length >= MAX_KEYS_PER_STEP) { showTemporaryError(m.macro_max_steps_error({ max: MAX_KEYS_PER_STEP })); return; } newSteps[stepIndex].keys = [...keysArray, option.value]; } setMacro({ ...macro, steps: newSteps }); if (errors.steps?.[stepIndex]?.keys) { const newErrors = { ...errors }; delete newErrors.steps?.[stepIndex].keys; if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { delete newErrors.steps?.[stepIndex]; } if (Object.keys(newErrors.steps || {}).length === 0) { delete newErrors.steps; } setErrors(newErrors); } }; const handleKeyQueryChange = (stepIndex: number, query: string) => { setKeyQueries(prev => ({ ...prev, [stepIndex]: query })); }; const handleModifierChange = (stepIndex: number, modifiers: string[]) => { const newSteps = [...(macro.steps || [])]; newSteps[stepIndex].modifiers = modifiers; setMacro({ ...macro, steps: newSteps }); // Clear step errors when modifiers are added if (errors.steps?.[stepIndex]?.keys && modifiers.length > 0) { const newErrors = { ...errors }; delete newErrors.steps?.[stepIndex].keys; if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { delete newErrors.steps?.[stepIndex]; } if (Object.keys(newErrors.steps || {}).length === 0) { delete newErrors.steps; } setErrors(newErrors); } }; const handleDelayChange = (stepIndex: number, delay: number) => { const newSteps = [...(macro.steps || [])]; newSteps[stepIndex].delay = delay; setMacro({ ...macro, steps: newSteps }); }; const handleStepMove = (stepIndex: number, direction: "up" | "down") => { const newSteps = [...(macro.steps || [])]; const newIndex = direction === "up" ? stepIndex - 1 : stepIndex + 1; [newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]]; setMacro({ ...macro, steps: newSteps }); }; const isMaxStepsReached = (macro.steps?.length || 0) >= MAX_STEPS_PER_MACRO; return (
{ setMacro(prev => ({ ...prev, name: e.target.value })); if (errors.name) { const newErrors = { ...errors }; delete newErrors.name; setErrors(newErrors); } }} />
{m.macro_step_count({ steps: macro.steps?.length || 0, max: MAX_STEPS_PER_MACRO })}
{errors.steps?.[0]?.keys && (
)}
{(macro.steps || []).map((step, stepIndex) => ( 1 ? () => { const newSteps = [...(macro.steps || [])]; newSteps.splice(stepIndex, 1); setMacro(prev => ({ ...prev, steps: newSteps })); } : undefined } onMoveUp={() => handleStepMove(stepIndex, "up")} onMoveDown={() => handleStepMove(stepIndex, "down")} onKeySelect={option => handleKeySelect(stepIndex, option)} onKeyQueryChange={query => handleKeyQueryChange(stepIndex, query)} keyQuery={keyQueries[stepIndex] || ""} onModifierChange={modifiers => handleModifierChange(stepIndex, modifiers) } onDelayChange={delay => handleDelayChange(stepIndex, delay)} isLastStep={stepIndex === (macro.steps?.length || 0) - 1} keyboard={selectedKeyboard} /> ))}
{errorMessage && (
)}
); }