import { useCallback, useEffect, useState } from 'react'; import { useI18n } from '@/application/i18n/I18nProvider'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { netcattyBridge } from '@/infrastructure/services/netcattyBridge.ts'; import type { ScriptDialogCondition, ScriptDialogField, ScriptDialogForm, ScriptDialogFormValue, ScriptDialogRequest, } from '@/types/global/netcatty-bridge-script.d.ts'; type FormValues = Record; type FormErrors = Record; type FormValidationMessages = string | { required: string; numberInvalid: string; numberMin: (min: number) => string; numberMax: (max: number) => string; numberStep: (step: number) => string; }; export function getInitialFormValues(request: ScriptDialogRequest): FormValues { if (request.type !== 'form' || !request.form) return {}; return Object.fromEntries( request.form.fields.map((field) => [field.name, field.defaultValue]), ); } export function applyFormValue(values: FormValues, name: string, value: ScriptDialogFormValue): FormValues { return { ...values, [name]: value }; } export function getDialogFieldDomId(name: string): string { return `script-dialog-${encodeURIComponent(name || 'field')}`; } function resolveValidationMessages(messages: FormValidationMessages = 'Required') { if (typeof messages === 'string') { return { required: messages, numberInvalid: messages, numberMin: () => messages, numberMax: () => messages, numberStep: () => messages, }; } return messages; } function matchesNumberStep(value: number, step: number, base = 0) { const quotient = (value - base) / step; return Math.abs(quotient - Math.round(quotient)) < 1e-9; } function getNumberStepBase(field: Extract) { return field.min ?? field.defaultValue ?? 0; } function getConditionFieldValue( form: ScriptDialogForm, values: FormValues, fieldName: string, ): ScriptDialogFormValue { const field = form.fields.find((candidate) => candidate.name === fieldName); const value = values[fieldName]; if (field?.type === 'number') { if (value === undefined || value === '') return undefined; const numberValue = typeof value === 'number' ? value : Number(value); return Number.isFinite(numberValue) ? numberValue : value; } if (field?.type === 'checkbox') { return Boolean(value); } return value; } export function evaluateDialogCondition( form: ScriptDialogForm, values: FormValues, condition: ScriptDialogCondition, ): boolean { const value = getConditionFieldValue(form, values, condition.field); if ('equals' in condition) { return value === condition.equals; } if ('notEquals' in condition) { return value !== condition.notEquals; } if ('truthy' in condition) { return Boolean(value); } return !value; } export function isDialogFieldVisible( form: ScriptDialogForm, field: ScriptDialogField, values: FormValues, ): boolean { return getVisibleDialogFormFields(form, values).some((visibleField) => visibleField.name === field.name); } export function getVisibleDialogFormFields(form: ScriptDialogForm, values: FormValues): ScriptDialogField[] { const visibleFields: ScriptDialogField[] = []; const visibleNames = new Set(); for (const field of form.fields) { if (!field.visibleWhen) { visibleFields.push(field); visibleNames.add(field.name); continue; } if (!visibleNames.has(field.visibleWhen.field)) { continue; } if (evaluateDialogCondition(form, values, field.visibleWhen)) { visibleFields.push(field); visibleNames.add(field.name); } } return visibleFields; } export function validateDialogFormValues( form: ScriptDialogForm, values: FormValues, messages: FormValidationMessages = 'Required', ): FormErrors { const validationMessages = resolveValidationMessages(messages); const errors: FormErrors = {}; for (const field of getVisibleDialogFormFields(form, values)) { const value = values[field.name]; if (field.type === 'checkbox') { if (field.required === true && !value) { errors[field.name] = validationMessages.required; } continue; } if (field.type === 'number') { const isEmpty = value === undefined || value === ''; if (isEmpty) { if (field.required !== false) { errors[field.name] = validationMessages.required; } continue; } const numberValue = typeof value === 'number' ? value : Number(value); if (!Number.isFinite(numberValue)) { errors[field.name] = validationMessages.numberInvalid; continue; } if (field.min !== undefined && numberValue < field.min) { errors[field.name] = validationMessages.numberMin(field.min); continue; } if (field.max !== undefined && numberValue > field.max) { errors[field.name] = validationMessages.numberMax(field.max); continue; } if (field.step !== undefined && !matchesNumberStep(numberValue, field.step, getNumberStepBase(field))) { errors[field.name] = validationMessages.numberStep(field.step); } continue; } if (field.required === false) continue; if (value === undefined || String(value).trim() === '') { errors[field.name] = validationMessages.required; } } return errors; } export function normalizeDialogFormSubmitValues(form: ScriptDialogForm, values: FormValues): FormValues { const next: FormValues = {}; for (const field of getVisibleDialogFormFields(form, values)) { const value = values[field.name]; if (field.type === 'number') { if (value === undefined || value === '') { next[field.name] = undefined; continue; } const numberValue = typeof value === 'number' ? value : Number(value); next[field.name] = Number.isFinite(numberValue) ? numberValue : undefined; continue; } next[field.name] = value; } return next; } export function ScriptDialogFormFields({ form, formValues, formErrors = {}, onValueChange, }: { form: ScriptDialogForm; formValues: FormValues; formErrors?: FormErrors; onValueChange: (name: string, value: ScriptDialogFormValue) => void; }) { const renderFormField = (field: ScriptDialogField) => { const inputId = getDialogFieldDomId(field.name); const fieldError = formErrors[field.name]; const descriptionId = field.description ? `${inputId}-description` : undefined; const errorId = fieldError ? `${inputId}-error` : undefined; const describedBy = [descriptionId, errorId].filter(Boolean).join(' ') || undefined; const fieldDescription = field.description ? (

{field.description}

) : null; const inlineDescription = field.description ? ( {field.description} ) : null; const errorMessage = fieldError ? (

{fieldError}

) : null; const inlineErrorMessage = fieldError ? ( {fieldError} ) : null; if (field.type === 'select') { const labelId = `${inputId}-label`; const selectedValue = String(formValues[field.name] ?? field.defaultValue); const selectedOption = field.options.find((option) => option.value === selectedValue); const selectedDescriptionId = selectedOption?.description ? `${inputId}-selected-description` : undefined; const selectDescribedBy = [describedBy, selectedDescriptionId].filter(Boolean).join(' ') || undefined; return (
{selectedOption?.description ? (

{selectedOption.description}

) : null} {fieldDescription} {errorMessage}
); } if (field.type === 'radio') { const selectedValue = String(formValues[field.name] ?? field.defaultValue); return (
{field.label} {fieldDescription}
{field.options.map((option, index) => { const optionInputId = `${inputId}-${index}`; return ( ); })}
{errorMessage}
); } if (field.type === 'textarea') { return (