import { Automation, AutomationDTO, HTTPOutput, OSCOutput, OntimeAction, isHTTPOutput, isOSCOutput, isOntimeAction, } from 'ontime-types'; import { useEffect, useMemo } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { IoAdd, IoTrash } from 'react-icons/io5'; import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation'; import { maybeAxiosError } from '../../../../common/api/utils'; import Button from '../../../../common/components/buttons/Button'; import IconButton from '../../../../common/components/buttons/IconButton'; import Info from '../../../../common/components/info/Info'; import Input from '../../../../common/components/input/input/Input'; import ExternalLink from '../../../../common/components/link/external-link/ExternalLink'; import RadioGroup from '../../../../common/components/radio-group/RadioGroup'; import Select from '../../../../common/components/select/Select'; 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 { isAutomation, makeFieldList } from './automationUtils'; import OntimeActionForm from './OntimeActionForm'; import TemplateInput from './template-input/TemplateInput'; 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({ automation, onClose }: AutomationFormProps) { const isEdit = isAutomation(automation); const { data } = useCustomFields(); const { refetch } = useAutomationSettings(); const fieldList = useMemo(() => makeFieldList(data), [data]); const { control, handleSubmit, getValues, register, setError, setFocus, setValue, watch, 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 = () => { appendOutput({ type: 'ontime', action: 'aux1-start' }); }; 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 (
 
removeFilter(index)}>
); })}

Outputs

Automation outputs can be used to send data from Ontime to external software
or to change properties of Ontime itself.

Use Ontime runtime data in these fields with template strings. Type {'{{'} to see autocomplete, or{' '} read the docs
{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
  removeOutput(index)}>
); } if (isHTTPOutput(output)) { const rowErrors = errors.outputs?.[index] as | { url?: { message?: string }; } | undefined; return (
HTTP
  removeOutput(index)}>
); } 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   removeOutput(index)}>
); } // there should be no other output types return null; })}
{errors?.root && {errors.root.message}}
); }