diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx index f20036324..2b8e18834 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.tsx @@ -8,9 +8,9 @@ import { isOSCOutput, isOntimeAction, } from 'ontime-types'; -import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; -import { IoAdd, IoCheckmark, IoTrash } from 'react-icons/io5'; +import { IoAdd, IoTrash } from 'react-icons/io5'; import { addAutomation, @@ -32,11 +32,12 @@ 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 { startsWithHttp } from '../../../../common/utils/regex'; import * as Panel from '../../panel-utils/PanelUtils'; -import { cycles, isAutomation, makeFieldList, operators } from './automationUtils'; +import { cycles, isAutomation, makeFieldList, operators, type OutputErrors } from './automationUtils'; +import HttpOutputForm from './HttpOutputForm'; import OntimeActionForm from './OntimeActionForm'; -import TemplateInput from './template-input/TemplateInput'; +import OscOutputForm from './OscOutputForm'; +import OutputCard, { type TestState } from './OutputCard'; import style from './AutomationForm.module.scss'; @@ -46,8 +47,6 @@ const formId = 'automation-form'; /** how long a successful test keeps its confirmation on screen */ const testFeedbackDuration = 2000; -type TestState = { status: 'sending' | 'ok' | 'error'; message?: string }; - /** lifecycles that fire continuously, and are worth a warning before a user picks one */ const continuousCycles: TimerLifeCycle[] = [TimerLifeCycle.onClock, TimerLifeCycle.onUpdate]; @@ -175,6 +174,8 @@ export default function AutomationForm({ automation, triggers, defaultCycles, on } }; + const getOutputErrors = (index: number) => errors.outputs?.[index] as OutputErrors | undefined; + const handleAddNewFilter = () => { appendFilter({ field: '', operator: 'equals', value: '' }); }; @@ -451,126 +452,38 @@ export default function AutomationForm({ automation, triggers, defaultCycles, on )} {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; + const rowErrors = getOutputErrors(index); + const cardProps = { + testState: testResults[output.id], + onTest: () => handleTest(index, output.id), + onDelete: () => removeOutput(index), + }; + if (isOSCOutput(output)) { return ( handleTest(index, output.id)} - onDelete={() => removeOutput(index)} + {...cardProps} > - - - - + ); } if (isHTTPOutput(output)) { - const rowErrors = errors.outputs?.[index] as - | { - url?: { message?: string }; - } - | undefined; return ( - handleTest(index, output.id)} - onDelete={() => 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 ( - handleTest(index, output.id)} - onDelete={() => removeOutput(index)} - > + ); } - -interface OutputCardProps { - label: string; - kindClass?: string; - summary?: string; - testState?: TestState; - onTest: () => void; - onDelete: () => void; - children: ReactNode; -} - -/** - * Shared chrome for every output kind: the type tag and the actions live in the header, - * so they stop competing with the form fields for grid columns - */ -function OutputCard({ label, kindClass, summary, testState, onTest, onDelete, children }: OutputCardProps) { - return ( -
-
- {label} - {summary} - {testState?.status === 'ok' && ( - - - {testState.message} - - )} - - - - -
- {testState?.status === 'error' && {testState.message}} -
{children}
-
- ); -} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/HttpOutputForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/HttpOutputForm.tsx new file mode 100644 index 000000000..349815a71 --- /dev/null +++ b/apps/client/src/features/app-settings/panel/automations-panel/HttpOutputForm.tsx @@ -0,0 +1,34 @@ +import type { AutomationDTO, HTTPOutput } from 'ontime-types'; +import type { UseFormRegister } from 'react-hook-form'; + +import { startsWithHttp } from '../../../../common/utils/regex'; +import * as Panel from '../../panel-utils/PanelUtils'; +import type { OutputErrors } from './automationUtils'; +import TemplateInput from './template-input/TemplateInput'; + +import style from './AutomationForm.module.scss'; + +interface HttpOutputFormProps { + index: number; + output: HTTPOutput; + register: UseFormRegister; + rowErrors?: OutputErrors; +} + +export default function HttpOutputForm({ index, output, register, rowErrors }: HttpOutputFormProps) { + return ( + + ); +} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx index e2cd556ec..abc861e86 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx @@ -5,6 +5,7 @@ import { UseFormRegister, UseFormSetValue, UseFormWatch } from 'react-hook-form' import Input from '../../../../common/components/input/input/Input'; import Select from '../../../../common/components/select/Select'; import * as Panel from '../../panel-utils/PanelUtils'; +import type { OutputErrors } from './automationUtils'; import TemplateInput from './template-input/TemplateInput'; import style from './AutomationForm.module.scss'; @@ -12,13 +13,7 @@ import style from './AutomationForm.module.scss'; interface OntimeActionFormProps { index: number; register: UseFormRegister; - rowErrors?: { - action?: { message?: string }; - time?: { message?: string }; - text?: { message?: string }; - visible?: { message?: string }; - secondarySource?: { message?: string }; - }; + rowErrors?: OutputErrors; value: OntimeAction['action']; watch: UseFormWatch; setValue: UseFormSetValue; diff --git a/apps/client/src/features/app-settings/panel/automations-panel/OscOutputForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/OscOutputForm.tsx new file mode 100644 index 000000000..5dcfd4951 --- /dev/null +++ b/apps/client/src/features/app-settings/panel/automations-panel/OscOutputForm.tsx @@ -0,0 +1,63 @@ +import type { AutomationDTO, OSCOutput } from 'ontime-types'; +import type { UseFormRegister } from 'react-hook-form'; + +import Input from '../../../../common/components/input/input/Input'; +import * as Panel from '../../panel-utils/PanelUtils'; +import type { OutputErrors } from './automationUtils'; +import TemplateInput from './template-input/TemplateInput'; + +import style from './AutomationForm.module.scss'; + +interface OscOutputFormProps { + index: number; + output: OSCOutput; + register: UseFormRegister; + rowErrors?: OutputErrors; +} + +export default function OscOutputForm({ index, output, register, rowErrors }: OscOutputFormProps) { + return ( + <> + + + + + + ); +} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/OutputCard.tsx b/apps/client/src/features/app-settings/panel/automations-panel/OutputCard.tsx new file mode 100644 index 000000000..8c2947c2f --- /dev/null +++ b/apps/client/src/features/app-settings/panel/automations-panel/OutputCard.tsx @@ -0,0 +1,58 @@ +import type { ReactNode } from 'react'; +import { IoCheckmark, IoTrash } from 'react-icons/io5'; + +import Button from '../../../../common/components/buttons/Button'; +import IconButton from '../../../../common/components/buttons/IconButton'; +import Tag from '../../../../common/components/tag/Tag'; +import * as Panel from '../../panel-utils/PanelUtils'; + +import style from './AutomationForm.module.scss'; + +export type TestState = { status: 'sending' | 'ok' | 'error'; message?: string }; + +interface OutputCardProps { + label: string; + kindClass?: string; + summary?: string; + testState?: TestState; + onTest: () => void; + onDelete: () => void; + children: ReactNode; +} + +/** + * Shared chrome for every output kind: the type tag and the actions live in the header, + * so they stop competing with the form fields for grid columns + */ +export default function OutputCard({ + label, + kindClass, + summary, + testState, + onTest, + onDelete, + children, +}: OutputCardProps) { + return ( +
+
+ {label} + {summary} + {testState?.status === 'ok' && ( + + + {testState.message} + + )} + + + + +
+ {testState?.status === 'error' && {testState.message}} +
{children}
+
+ ); +} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts b/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts index 7978f42e4..4685ed6ea 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts +++ b/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts @@ -2,6 +2,12 @@ import { Automation, AutomationDTO, AutomationFilter, CustomFields, TimerLifeCyc import { lifecycleLabels } from '../../../../common/constants/timerLifecycle'; +/** + * Outputs are a union, so react-hook-form cannot resolve a field's error by name. + * Every output card knows which fields it registered, this just makes them reachable. + */ +export type OutputErrors = Partial>; + type CycleLabel = { id: number; label: string;