import { useEffect, useMemo } from 'react'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { IoAdd, IoTrash } from 'react-icons/io5'; import { Button, IconButton, Input, Radio, RadioGroup, Select } from '@chakra-ui/react'; import { Automation, AutomationDTO, HTTPOutput, isHTTPOutput, isOntimeAction, isOSCOutput, OntimeAction, OSCOutput, } from 'ontime-types'; import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation'; import { maybeAxiosError } from '../../../../common/api/utils'; import Info from '../../../../common/components/info/Info'; import ExternalLink from '../../../../common/components/link/external-link/ExternalLink'; import Tag from '../../../../common/components/tag/Tag'; import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings'; import useCustomFields from '../../../../common/hooks-query/useCustomFields'; import { preventEscape } from '../../../../common/utils/keyEvent'; import { startsWithHttp } from '../../../../common/utils/regex'; import * as Panel from '../../panel-utils/PanelUtils'; import TemplateInput from './template-input/TemplateInput'; import { isAutomation, makeFieldList } from './automationUtils'; import OntimeActionForm from './OntimeActionForm'; import style from './AutomationForm.module.scss'; const integrationsDocsUrl = 'https://docs.getontime.no/api/automation/#using-variables-in-automation'; interface AutomationFormProps { automation: Automation | AutomationDTO; onClose: () => void; } export default function AutomationForm(props: AutomationFormProps) { const { automation, onClose } = props; const isEdit = isAutomation(automation); const { data } = useCustomFields(); const { refetch } = useAutomationSettings(); const fieldList = useMemo(() => makeFieldList(data), [data]); const { control, handleSubmit, getValues, register, setError, setFocus, setValue, formState: { errors, isSubmitting, isDirty, isValid }, } = useForm({ mode: 'onChange', defaultValues: { title: automation?.title ?? '', filterRule: automation?.filterRule ?? 'all', filters: automation?.filters ?? [], outputs: automation?.outputs ?? [], }, resetOptions: { keepDirtyValues: true, }, }); const { fields: fieldFilters, append: appendFilter, remove: removeFilter, } = useFieldArray({ name: 'filters', control, }); const { fields: fieldOutputs, append: appendOutput, remove: removeOutput, } = useFieldArray({ name: 'outputs', control, }); // give initial focus to the title field useEffect(() => { setFocus('title'); }, [setFocus]); const handleAddNewFilter = () => { appendFilter({ field: '', operator: 'equals', value: '' }); }; const handleAddNewOSCOutput = () => { // @ts-expect-error -- we dont want to pass a port to the new object appendOutput({ type: 'osc', targetIP: '', targetPort: undefined, address: '', args: '' }); }; const handleAddNewHTTPOutput = () => { appendOutput({ type: 'http', url: '' }); }; const handleAddnewOntimeAction = () => { // @ts-expect-error -- we dont want to choose an action appendOutput({ type: 'ontime', action: undefined }); }; const handleTestOSCOutput = async (index: number) => { try { const values = getValues(`outputs.${index}`) as OSCOutput; if (!values.targetIP || !values.targetPort || !values.address) { return; } await testOutput({ type: 'osc', targetIP: values.targetIP, targetPort: values.targetPort, address: values.address, args: values.args, }); } catch (_error) { /** we dont handle errors here, users should use the network tab */ } }; const handleTestHTTPOutput = async (index: number) => { try { const values = getValues(`outputs.${index}`) as HTTPOutput; if (!values.url) { return; } await testOutput({ type: 'http', url: values.url, }); } catch (_error) { /** we dont handle errors here, users should use the network tab */ } }; const handleTestOntimeAction = async (index: number) => { try { const values = getValues(`outputs.${index}`) as OntimeAction; // NOTE: there is no meaningful validation to do here, we let the server deal with the data await testOutput({ ...values, type: 'ontime', }); } catch (_error) { /** we dont handle errors here */ } }; const onSubmit = async (values: AutomationDTO) => { if (isAutomation(automation)) { await handleEdit(automation.id, { id: automation.id, ...values }); } else { await handleCreate(values); } refetch(); async function handleEdit(id: string, values: Automation) { try { await editAutomation(id, values); onClose(); } catch (error) { setError('root', { message: maybeAxiosError(error) }); } } async function handleCreate(values: AutomationDTO) { try { await addAutomation(values); onClose(); } catch (error) { setError('root', { message: maybeAxiosError(error) }); } } }; const canSubmit = !isSubmitting && isDirty && isValid; return ( preventEscape(event, onClose)} > {isEdit ? 'Edit automation' : 'Create automation'}

Automation options

{errors.title?.message}

Filters (optional)

{fieldFilters.map((field, index) => { const key = `filters.${index}.field.${field.id}`; return (
 
} variant='ontime-ghosted' size='sm' color='#FA5656' // $red-500 onClick={() => removeFilter(index)} isDisabled={false} isLoading={false} />
); })}

Outputs

Automation outputs can be used to send data from Ontime to external software. See the documentation for templates {fieldOutputs.map((output, index) => { if (isOSCOutput(output)) { const rowErrors = errors.outputs?.[index] as | { targetIP?: { message?: string }; targetPort?: { message?: string }; address?: { message?: string }; args?: { message?: string }; } | undefined; return (
OSC
  } variant='ontime-ghosted' size='sm' onClick={() => removeOutput(index)} color='#FA5656' // $red-500 />
); } if (isHTTPOutput(output)) { const rowErrors = errors.outputs?.[index] as | { url?: { message?: string }; } | undefined; return (
HTTP
  } variant='ontime-ghosted' size='sm' onClick={() => removeOutput(index)} color='#FA5656' // $red-500 />
); } if (isOntimeAction(output)) { const rowErrors = errors.outputs?.[index] as | { action?: { message?: string }; time?: { message?: string }; text?: { message?: string }; visible?: { message?: string }; secondarySource?: { message?: string }; } | undefined; return (
Ontime action   } variant='ontime-ghosted' size='sm' onClick={() => removeOutput(index)} color='#FA5656' // $red-500 />
); } // there should be no other output types return null; })}
{errors?.root && {errors.root.message}}
); }