From f9b9e64a8aff3bd92a8cf67e49f58d40f05fa894 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Mon, 20 Jan 2025 19:22:00 +0100 Subject: [PATCH] chore: rename feature elements rename automations > triggers rename blueprints > automations chore: add link to documentation --- apps/client/src/common/api/automation.ts | 40 +- .../src/common/models/AutomationSettings.ts | 4 +- ...module.scss => AutomationForm.module.scss} | 0 .../automations-panel/AutomationForm.tsx | 495 +++++++++++++++--- .../automations-panel/AutomationPanel.tsx | 14 +- .../AutomationSettingsForm.tsx | 4 +- .../automations-panel/AutomationsList.tsx | 157 +++--- .../panel/automations-panel/BlueprintForm.tsx | 483 ----------------- .../automations-panel/BlueprintsList.tsx | 130 ----- .../panel/automations-panel/TriggerForm.tsx | 139 +++++ .../panel/automations-panel/TriggersList.tsx | 117 +++++ ...tionsListItem.tsx => TriggersListItem.tsx} | 21 +- .../__tests__/automationUtils.test.ts | 24 +- .../automations-panel/automationUtils.ts | 28 +- .../app-settings/useAppSettingsMenu.tsx | 2 +- .../__tests__/automation.dao.test.ts | 152 +++--- .../__tests__/automation.service.test.ts | 20 +- .../automation/automation.controller.ts | 36 +- .../src/api-data/automation/automation.dao.ts | 138 +++-- .../api-data/automation/automation.parser.ts | 18 +- .../api-data/automation/automation.router.ts | 24 +- .../api-data/automation/automation.service.ts | 20 +- .../automation/automation.validation.ts | 44 +- .../__tests__/DataProvider.utils.test.ts | 8 +- apps/server/src/models/dataModel.ts | 4 +- apps/server/src/models/demoProject.ts | 4 +- apps/server/test-db/db.json | 4 +- e2e/tests/fixtures/test-db.json | 4 +- .../src/definitions/core/Automation.type.ts | 20 +- packages/types/src/index.ts | 8 +- 30 files changed, 1066 insertions(+), 1096 deletions(-) rename apps/client/src/features/app-settings/panel/automations-panel/{BlueprintForm.module.scss => AutomationForm.module.scss} (100%) delete mode 100644 apps/client/src/features/app-settings/panel/automations-panel/BlueprintForm.tsx delete mode 100644 apps/client/src/features/app-settings/panel/automations-panel/BlueprintsList.tsx create mode 100644 apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/automations-panel/TriggersList.tsx rename apps/client/src/features/app-settings/panel/automations-panel/{AutomationsListItem.tsx => TriggersListItem.tsx} (77%) diff --git a/apps/client/src/common/api/automation.ts b/apps/client/src/common/api/automation.ts index fb41105ad2..e6c031989d 100644 --- a/apps/client/src/common/api/automation.ts +++ b/apps/client/src/common/api/automation.ts @@ -1,11 +1,11 @@ import axios from 'axios'; import type { Automation, - AutomationBlueprint, - AutomationBlueprintDTO, AutomationDTO, AutomationOutput, AutomationSettings, + Trigger, + TriggerDTO, } from 'ontime-types'; import { apiEntryUrl } from './constants'; @@ -31,49 +31,49 @@ export async function editAutomationSettings( } /** - * HTTP request to create a new automation + * HTTP request to create a new automation trigger */ -export async function addAutomation(automation: AutomationDTO): Promise { - const res = await axios.post(`${automationsPath}/automation`, automation); +export async function addTrigger(trigger: TriggerDTO): Promise { + const res = await axios.post(`${automationsPath}/trigger`, trigger); return res.data; } /** - * HTTP request to update an automation + * HTTP request to update an automation trigger */ -export async function editAutomation(id: string, automation: Automation): Promise { - const res = await axios.put(`${automationsPath}/automation/${id}`, automation); +export async function editTrigger(id: string, trigger: Trigger): Promise { + const res = await axios.put(`${automationsPath}/trigger/${id}`, trigger); return res.data; } /** - * HTTP request to delete an automation + * HTTP request to delete an automation trigger */ -export function deleteAutomation(id: string): Promise { - return axios.delete(`${automationsPath}/automation/${id}`); +export function deleteTrigger(id: string): Promise { + return axios.delete(`${automationsPath}/trigger/${id}`); } /** - * HTTP request to create a new blueprint + * HTTP request to create a new automation */ -export async function addBlueprint(blueprint: AutomationBlueprintDTO): Promise { - const res = await axios.post(`${automationsPath}/blueprint`, blueprint); +export async function addAutomation(automation: AutomationDTO): Promise { + const res = await axios.post(`${automationsPath}/automation`, automation); return res.data; } /** - * HTTP request to update a blueprint + * HTTP request to update a automation */ -export async function editBlueprint(id: string, blueprint: AutomationBlueprint): Promise { - const res = await axios.put(`${automationsPath}/blueprint/${id}`, blueprint); +export async function editAutomation(id: string, automation: Automation): Promise { + const res = await axios.put(`${automationsPath}/automation/${id}`, automation); return res.data; } /** - * HTTP request to delete a blueprint + * HTTP request to delete a automation */ -export function deleteBlueprint(id: string): Promise { - return axios.delete(`${automationsPath}/blueprint/${id}`); +export function deleteAutomation(id: string): Promise { + return axios.delete(`${automationsPath}/automation/${id}`); } /** diff --git a/apps/client/src/common/models/AutomationSettings.ts b/apps/client/src/common/models/AutomationSettings.ts index 99691df3dd..c918bb7d8b 100644 --- a/apps/client/src/common/models/AutomationSettings.ts +++ b/apps/client/src/common/models/AutomationSettings.ts @@ -4,6 +4,6 @@ export const automationPlaceholderSettings: AutomationSettings = { enabledAutomations: false, enabledOscIn: false, oscPortIn: 8888, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }; diff --git a/apps/client/src/features/app-settings/panel/automations-panel/BlueprintForm.module.scss b/apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.module.scss similarity index 100% rename from apps/client/src/features/app-settings/panel/automations-panel/BlueprintForm.module.scss rename to apps/client/src/features/app-settings/panel/automations-panel/AutomationForm.module.scss 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 39bb02eaa2..853e998c00 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 @@ -1,136 +1,457 @@ -import { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { Button, Input, Select } from '@chakra-ui/react'; -import { AutomationDTO, NormalisedAutomationBlueprint, TimerLifeCycle } from 'ontime-types'; +import { useEffect, useMemo } from 'react'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import { + Alert, + AlertDescription, + AlertIcon, + Button, + IconButton, + Input, + Radio, + RadioGroup, + Select, +} from '@chakra-ui/react'; +import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; +import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; +import { Automation, AutomationDTO, HTTPOutput, isHTTPOutput, isOSCOutput, OSCOutput } from 'ontime-types'; -import { addAutomation, editAutomation } from '../../../../common/api/automation'; +import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation'; import { maybeAxiosError } from '../../../../common/api/utils'; +import ExternalLink from '../../../../common/components/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 { cycles } from './automationUtils'; +import { isAutomation, makeFieldList } from './automationUtils'; + +import style from './AutomationForm.module.scss'; + +const integrationsDocsUrl = 'https://docs.getontime.no/api/integrations/#using-variables-in-integrations'; interface AutomationFormProps { - blueprints: NormalisedAutomationBlueprint; - initialId?: string; - initialTitle?: string; - initialBlueprint?: string; - initialTrigger?: TimerLifeCycle; - onCancel: () => void; - postSubmit: () => void; + automation: Automation | AutomationDTO; + onClose: () => void; } export default function AutomationForm(props: AutomationFormProps) { - const { blueprints, initialId, initialTitle, initialBlueprint, initialTrigger, onCancel, postSubmit } = props; + 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, - setFocus, setError, - formState: { errors, isSubmitting, isValid, isDirty }, + setFocus, + formState: { errors, isSubmitting, isDirty, isValid }, } = useForm({ + mode: 'onChange', defaultValues: { - title: initialTitle, - trigger: initialTrigger, - blueprintId: initialBlueprint, + 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'); - // eslint-disable-next-line react-hooks/exhaustive-deps -- focus on mount - }, []); + }, [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 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 onSubmit = async (values: AutomationDTO) => { - // if we were passed an ID we are editing a blueprint - if (initialId) { + 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(initialId, { id: initialId, ...values }); - postSubmit(); + await editAutomation(id, values); + onClose(); } catch (error) { - setError('root', { message: `Failed to save changes to automation ${maybeAxiosError(error)}` }); + setError('root', { message: maybeAxiosError(error) }); } - return; } - // otherwise we are creating a new automation - try { - await addAutomation(values); - postSubmit(); - } catch (error) { - setError('root', { message: `Failed to save automation ${maybeAxiosError(error)}` }); + async function handleCreate(values: AutomationDTO) { + try { + await addAutomation(values); + onClose(); + } catch (error) { + setError('root', { message: maybeAxiosError(error) }); + } } }; - const blueprintSelect = Object.keys(blueprints).map((blueprint) => { - return { - value: blueprint, - label: blueprints[blueprint].title, - }; - }); - - const canSubmit = isDirty && isValid; + const canSubmit = !isSubmitting && isDirty && isValid; return ( preventEscape(event, onCancel)} + className={style.outerColumn} + onKeyDown={(event) => preventEscape(event, onClose)} > - {initialId ? 'Edit automation' : 'Create automation'} - - - + {errors.title?.message} + + + +
+

Filters

+
+ + {fieldFilters.map((field, index) => ( +
+ + + + } + variant='ontime-ghosted' + size='sm' + color='#FA5656' // $red-500 + onClick={() => removeFilter(index)} + isDisabled={false} + isLoading={false} + /> +
))} - - {errors.blueprintId?.message} - +
+ +
+
+
+ +
+

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 + isDisabled={false} + isLoading={false} + /> + +
+
+ ); + } + 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 + isDisabled={false} + isLoading={false} + /> + +
+
+ ); + } + // there should be no other output types + return null; + })} + + + + +
+ - - diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationPanel.tsx b/apps/client/src/features/app-settings/panel/automations-panel/AutomationPanel.tsx index 64df2a6b71..9f3597d294 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationPanel.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/AutomationPanel.tsx @@ -5,13 +5,13 @@ import * as Panel from '../../panel-utils/PanelUtils'; import AutomationSettingsForm from './AutomationSettingsForm'; import AutomationsList from './AutomationsList'; -import BlueprintsList from './BlueprintsList'; +import TriggersList from './TriggersList'; export default function AutomationPanel({ location }: PanelBaseProps) { const { data, status } = useAutomationSettings(); const settingsRef = useScrollIntoView('settings', location); - const automationRef = useScrollIntoView('automations', location); - const blueprintsRef = useScrollIntoView('blueprints', location); + const triggersRef = useScrollIntoView('triggers', location); + const automationsRef = useScrollIntoView('automations', location); const isLoading = status === 'pending'; @@ -27,11 +27,11 @@ export default function AutomationPanel({ location }: PanelBaseProps) { oscPortIn={data.oscPortIn} /> -
- +
+
-
- +
+
diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx index ce04657041..2357720cda 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx @@ -62,7 +62,7 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) { variant='ontime-filled' size='sm' type='submit' - form='automation-form' + form='automation-settings-form' isDisabled={!canSubmit} isLoading={isSubmitting} > @@ -87,7 +87,7 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) { preventEscape(event, onReset)} > diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationsList.tsx b/apps/client/src/features/app-settings/panel/automations-panel/AutomationsList.tsx index e6ccdf02e2..8c4ba6ffc2 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationsList.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/AutomationsList.tsx @@ -1,30 +1,38 @@ -import { Fragment, useMemo, useState } from 'react'; -import { Button } from '@chakra-ui/react'; +import { Fragment, useState } from 'react'; +import { Button, IconButton } from '@chakra-ui/react'; import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; -import { Automation, NormalisedAutomationBlueprint } from 'ontime-types'; +import { IoPencil } from '@react-icons/all-files/io5/IoPencil'; +import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; +import { AutomationDTO, NormalisedAutomation } from 'ontime-types'; import { deleteAutomation } from '../../../../common/api/automation'; import { maybeAxiosError } from '../../../../common/api/utils'; +import Tag from '../../../../common/components/tag/Tag'; import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings'; import * as Panel from '../../panel-utils/PanelUtils'; import AutomationForm from './AutomationForm'; -import AutomationsListItem from './AutomationsListItem'; -import { checkDuplicates } from './automationUtils'; + +const automationPlaceholder: AutomationDTO = { + title: '', + filterRule: 'all', + filters: [], + outputs: [], +}; interface AutomationsListProps { - automations: Automation[]; - blueprints: NormalisedAutomationBlueprint; + automations: NormalisedAutomation; } export default function AutomationsList(props: AutomationsListProps) { - const { automations, blueprints } = props; - const [showForm, setShowForm] = useState(false); + const { automations } = props; const { refetch } = useAutomationSettings(); + const [automationFormData, setAutomationFormData] = useState(null); const [deleteError, setDeleteError] = useState(null); const handleDelete = async (id: string) => { try { + setDeleteError(null); await deleteAutomation(id); } catch (error) { setDeleteError(maybeAxiosError(error)); @@ -33,15 +41,7 @@ export default function AutomationsList(props: AutomationsListProps) { } }; - const postSubmit = () => { - setShowForm(false); - refetch(); - }; - - const duplicates = useMemo(() => checkDuplicates(automations), [automations]); - - // there is no point letting user creating an automation if there are no blueprints - const canAdd = Object.keys(blueprints).length > 0; + const arrayAutomations = Object.keys(automations); return ( @@ -52,66 +52,77 @@ export default function AutomationsList(props: AutomationsListProps) { rightIcon={} size='sm' type='submit' - form='automation-form' - isDisabled={!canAdd} - isLoading={false} - onClick={() => setShowForm(true)} + isDisabled={Boolean(automationFormData)} + onClick={() => setAutomationFormData(automationPlaceholder)} > New + - - {duplicates && ( - - You have created multiple links between the same trigger and blueprint which can performance issues. - - )} - {showForm && ( - setShowForm(false)} postSubmit={postSubmit} /> - )} - - - - Title - Trigger - Blueprint - - - - - {!showForm && automations.length === 0 && ( - setShowForm(true) : undefined} - /> - )} - {automations.map((automation, index) => { - return ( - - handleDelete(automation.id)} - postSubmit={postSubmit} - /> - {deleteError && ( - - - {deleteError} - - - )} - - ); - })} - - - + + {automationFormData !== null && ( + setAutomationFormData(null)} /> + )} + + + + + Title + Trigger rule + Filters + Outputs + + + + + {arrayAutomations.length === 0 && ( + setAutomationFormData(automationPlaceholder)} /> + )} + {arrayAutomations.map((automationId) => { + if (!Object.hasOwn(automations, automationId)) { + return null; + } + return ( + + + {automations[automationId].title} + + {automations[automationId].filterRule} + + {automations[automationId].filters.length} + {automations[automationId].outputs.length} + + } + aria-label='Edit entry' + onClick={() => setAutomationFormData(automations[automationId])} + /> + } + aria-label='Delete entry' + onClick={() => handleDelete(automationId)} + /> + + + {deleteError && ( + + + {deleteError} + + + )} + + ); + })} + + ); } diff --git a/apps/client/src/features/app-settings/panel/automations-panel/BlueprintForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/BlueprintForm.tsx deleted file mode 100644 index 9472c6f821..0000000000 --- a/apps/client/src/features/app-settings/panel/automations-panel/BlueprintForm.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import { useEffect, useMemo } from 'react'; -import { Controller, useFieldArray, useForm } from 'react-hook-form'; -import { Button, IconButton, Input, Radio, RadioGroup, Select } from '@chakra-ui/react'; -import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; -import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; -import { - AutomationBlueprint, - AutomationBlueprintDTO, - CustomFields, - HTTPOutput, - isHTTPOutput, - isOSCOutput, - OntimeEvent, - OSCOutput, -} from 'ontime-types'; - -import { addBlueprint, editBlueprint, testOutput } from '../../../../common/api/automation'; -import { maybeAxiosError } from '../../../../common/api/utils'; -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 { isBlueprint, makeFieldList } from './automationUtils'; - -import style from './BlueprintForm.module.scss'; - -interface BlueprintFormProps { - blueprint: AutomationBlueprintDTO | AutomationBlueprint; - onClose: () => void; -} - -export default function BlueprintForm(props: BlueprintFormProps) { - const { blueprint, onClose } = props; - const isEdit = isBlueprint(blueprint); - const { data } = useCustomFields(); - const { refetch } = useAutomationSettings(); - const fieldList = useMemo(() => makeFieldList(data), [data]); - - const { - control, - handleSubmit, - getValues, - register, - setError, - setFocus, - formState: { errors, isSubmitting, isDirty, isValid }, - } = useForm({ - mode: 'onChange', - defaultValues: { - title: blueprint?.title ?? '', - filterRule: blueprint?.filterRule ?? 'all', - filters: blueprint?.filters ?? [], - outputs: blueprint?.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 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 onSubmit = async (values: AutomationBlueprintDTO) => { - if (isBlueprint(blueprint)) { - await handleEdit(blueprint.id, { id: blueprint.id, ...values }); - } else { - await handleCreate(values); - } - refetch(); - - async function handleEdit(id: string, values: AutomationBlueprint) { - try { - await editBlueprint(id, values); - onClose(); - } catch (error) { - setError('root', { message: maybeAxiosError(error) }); - } - } - - async function handleCreate(values: AutomationBlueprintDTO) { - try { - await addBlueprint(values); - onClose(); - } catch (error) { - setError('root', { message: maybeAxiosError(error) }); - } - } - }; - - const canSubmit = !isSubmitting && isDirty && isValid; - - return ( - preventEscape(event, onClose)} - > - {isEdit ? 'Edit blueprint' : 'Create blueprint'} -
-

Blueprint options

-
- - {errors.title?.message} -
-
- -
-

Filters

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

Outputs

- {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 - isDisabled={false} - isLoading={false} - /> - -
-
- ); - } - 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 - isDisabled={false} - isLoading={false} - /> - -
-
- ); - } - // there should be no other output types - return null; - })} - - - - -
- - - {errors?.root && {errors.root.message}} - - - -
- ); -} - -/** - * We use this guard to find out if the form is receiving an existing blueprint or creating a DTO - * We do this by checking whether an ID has been generated - */ -function isBlueprint(blueprint: AutomationBlueprintDTO | AutomationBlueprint): blueprint is AutomationBlueprint { - return Object.hasOwn(blueprint, 'id'); -} - -export const staticSelectProperties = [ - { value: 'id', label: 'ID' }, - { value: 'title', label: 'Title' }, - { value: 'cue', label: 'Cue' }, - { value: 'countToEnd', label: 'Count to end' }, - { value: 'isPublic', label: 'Is public' }, - { value: 'skip', label: 'Skip' }, - { value: 'note', label: 'Note' }, - { value: 'colour', label: 'Colour' }, - { value: 'endAction', label: 'End action' }, - { value: 'timerType', label: 'Timer type' }, - { value: 'timeWarning', label: 'Time warning' }, - { value: 'timeDanger', label: 'Time danger' }, -]; - -type SelectableField = { - value: keyof OntimeEvent | string; // string for custom fields - label: string; -}; - -function makeFieldList(customFields: CustomFields): SelectableField[] { - return [ - ...staticSelectProperties, - ...Object.entries(customFields).map(([key, { label }]) => ({ value: key, label: `Custom: ${label}` })), - ]; -} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/BlueprintsList.tsx b/apps/client/src/features/app-settings/panel/automations-panel/BlueprintsList.tsx deleted file mode 100644 index a5e111511e..0000000000 --- a/apps/client/src/features/app-settings/panel/automations-panel/BlueprintsList.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Fragment, useState } from 'react'; -import { Button, IconButton } from '@chakra-ui/react'; -import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; -import { IoPencil } from '@react-icons/all-files/io5/IoPencil'; -import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; -import { AutomationBlueprintDTO, NormalisedAutomationBlueprint } from 'ontime-types'; - -import { deleteBlueprint } from '../../../../common/api/automation'; -import { maybeAxiosError } from '../../../../common/api/utils'; -import Tag from '../../../../common/components/tag/Tag'; -import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings'; -import * as Panel from '../../panel-utils/PanelUtils'; - -import BlueprintForm from './BlueprintForm'; - -const automationBlueprintPlaceholder: AutomationBlueprintDTO = { - title: '', - filterRule: 'all', - filters: [], - outputs: [], -}; - -interface BlueprintsListProps { - blueprints: NormalisedAutomationBlueprint; -} - -export default function BlueprintsList(props: BlueprintsListProps) { - const { blueprints } = props; - const { refetch } = useAutomationSettings(); - const [blueprintFormData, setBlueprintFormData] = useState( - null, - ); - const [deleteError, setDeleteError] = useState(null); - - const handleDelete = async (id: string) => { - try { - setDeleteError(null); - await deleteBlueprint(id); - } catch (error) { - setDeleteError(maybeAxiosError(error)); - } finally { - refetch(); - } - }; - - const arrayBlueprints = Object.keys(blueprints); - - return ( - - - Manage blueprints - - - - - - {blueprintFormData !== null && ( - setBlueprintFormData(null)} /> - )} - - - - - Title - Trigger rule - Filters - Outputs - - - - - {arrayBlueprints.length === 0 && ( - setBlueprintFormData(automationBlueprintPlaceholder)} /> - )} - {arrayBlueprints.map((blueprintId) => { - if (!Object.hasOwn(blueprints, blueprintId)) { - return null; - } - return ( - - - {blueprints[blueprintId].title} - - {blueprints[blueprintId].filterRule} - - {blueprints[blueprintId].filters.length} - {blueprints[blueprintId].outputs.length} - - } - aria-label='Edit entry' - onClick={() => setBlueprintFormData(blueprints[blueprintId])} - /> - } - aria-label='Delete entry' - onClick={() => handleDelete(blueprintId)} - /> - - - {deleteError && ( - - - {deleteError} - - - )} - - ); - })} - - - - ); -} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx new file mode 100644 index 0000000000..d7f5370f64 --- /dev/null +++ b/apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx @@ -0,0 +1,139 @@ +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { Button, Input, Select } from '@chakra-ui/react'; +import { NormalisedAutomation, TimerLifeCycle, TriggerDTO } from 'ontime-types'; + +import { addTrigger, editTrigger } from '../../../../common/api/automation'; +import { maybeAxiosError } from '../../../../common/api/utils'; +import { preventEscape } from '../../../../common/utils/keyEvent'; +import * as Panel from '../../panel-utils/PanelUtils'; + +import { cycles } from './automationUtils'; + +interface TriggerFormProps { + automations: NormalisedAutomation; + initialId?: string; + initialTitle?: string; + initialAutomationId?: string; + initialTrigger?: TimerLifeCycle; + onCancel: () => void; + postSubmit: () => void; +} + +export default function TriggerForm(props: TriggerFormProps) { + const { automations, initialId, initialTitle, initialAutomationId, initialTrigger, onCancel, postSubmit } = props; + const { + handleSubmit, + register, + setFocus, + setError, + formState: { errors, isSubmitting, isValid, isDirty }, + } = useForm({ + defaultValues: { + title: initialTitle, + trigger: initialTrigger, + automationId: initialAutomationId, + }, + resetOptions: { + keepDirtyValues: true, + }, + }); + + // give initial focus to the title field + useEffect(() => { + setFocus('title'); + // eslint-disable-next-line react-hooks/exhaustive-deps -- focus on mount + }, []); + + const onSubmit = async (values: TriggerDTO) => { + // if we were passed an ID we are editing a Trigger + if (initialId) { + try { + await editTrigger(initialId, { id: initialId, ...values }); + postSubmit(); + } catch (error) { + setError('root', { message: `Failed to save changes to trigger ${maybeAxiosError(error)}` }); + } + return; + } + + // otherwise we are creating a new automation + try { + await addTrigger(values); + postSubmit(); + } catch (error) { + setError('root', { message: `Failed to save trigger ${maybeAxiosError(error)}` }); + } + }; + + const automationSelect = Object.keys(automations).map((automation) => { + return { + value: automation, + label: automations[automation].title, + }; + }); + + const canSubmit = isDirty && isValid; + + return ( + preventEscape(event, onCancel)} + > + {initialId ? 'Edit trigger' : 'Create trigger'} + + + + + + + + + ); +} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/TriggersList.tsx b/apps/client/src/features/app-settings/panel/automations-panel/TriggersList.tsx new file mode 100644 index 0000000000..524169fb2d --- /dev/null +++ b/apps/client/src/features/app-settings/panel/automations-panel/TriggersList.tsx @@ -0,0 +1,117 @@ +import { Fragment, useMemo, useState } from 'react'; +import { Button } from '@chakra-ui/react'; +import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; +import { NormalisedAutomation, Trigger } from 'ontime-types'; + +import { deleteTrigger } from '../../../../common/api/automation'; +import { maybeAxiosError } from '../../../../common/api/utils'; +import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings'; +import * as Panel from '../../panel-utils/PanelUtils'; + +import { checkDuplicates } from './automationUtils'; +import AutomationForm from './TriggerForm'; +import TriggersListItem from './TriggersListItem'; + +interface TriggersListProps { + triggers: Trigger[]; + automations: NormalisedAutomation; +} + +export default function TriggersList(props: TriggersListProps) { + const { triggers, automations } = props; + const [showForm, setShowForm] = useState(false); + const { refetch } = useAutomationSettings(); + const [deleteError, setDeleteError] = useState(null); + + const handleDelete = async (id: string) => { + try { + await deleteTrigger(id); + } catch (error) { + setDeleteError(maybeAxiosError(error)); + } finally { + refetch(); + } + }; + + const postSubmit = () => { + setShowForm(false); + refetch(); + }; + + const duplicates = useMemo(() => checkDuplicates(triggers), [triggers]); + + // there is no point letting user creating a trigger if there are no automations + const canAdd = Object.keys(automations).length > 0; + + return ( + + + Manage triggers + + + + + {duplicates && ( + + You have created multiple links between the same trigger and automation which can performance issues. + + )} + {showForm && ( + setShowForm(false)} postSubmit={postSubmit} /> + )} + + + + Title + Lifecycle trigger + Automation + + + + + {!showForm && triggers.length === 0 && ( + setShowForm(true) : undefined} + /> + )} + {triggers.map((trigger, index) => { + return ( + + handleDelete(trigger.id)} + postSubmit={postSubmit} + /> + {deleteError && ( + + + {deleteError} + + + )} + + ); + })} + + + + + ); +} diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationsListItem.tsx b/apps/client/src/features/app-settings/panel/automations-panel/TriggersListItem.tsx similarity index 77% rename from apps/client/src/features/app-settings/panel/automations-panel/AutomationsListItem.tsx rename to apps/client/src/features/app-settings/panel/automations-panel/TriggersListItem.tsx index 3d0051ee50..89753506c4 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationsListItem.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/TriggersListItem.tsx @@ -3,27 +3,27 @@ import { IconButton } from '@chakra-ui/react'; import { IoPencil } from '@react-icons/all-files/io5/IoPencil'; import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; import { IoWarningOutline } from '@react-icons/all-files/io5/IoWarningOutline'; -import { NormalisedAutomationBlueprint, TimerLifeCycle } from 'ontime-types'; +import { NormalisedAutomation, TimerLifeCycle } from 'ontime-types'; import Tag from '../../../../common/components/tag/Tag'; import * as Panel from '../../panel-utils/PanelUtils'; -import AutomationForm from './AutomationForm'; import { cycles } from './automationUtils'; +import AutomationForm from './TriggerForm'; -interface AutomationsListItemProps { - blueprints: NormalisedAutomationBlueprint; +interface TriggersListItemProps { + automations: NormalisedAutomation; id: string; title: string; trigger: TimerLifeCycle; - blueprintId: string; + automationId: string; duplicate?: boolean; handleDelete: () => void; postSubmit: () => void; } -export default function AutomationsListItem(props: AutomationsListItemProps) { - const { blueprints, id, title, trigger, blueprintId, duplicate, handleDelete, postSubmit } = props; +export default function TriggersListItem(props: TriggersListItemProps) { + const { automations, id, title, trigger, automationId, duplicate, handleDelete, postSubmit } = props; const [isEditing, setIsEditing] = useState(false); if (isEditing) { @@ -31,11 +31,11 @@ export default function AutomationsListItem(props: AutomationsListItemProps) { setIsEditing(false)} postSubmit={() => { setIsEditing(false); @@ -47,7 +47,6 @@ export default function AutomationsListItem(props: AutomationsListItemProps) { ); } - const blueprintTitle = blueprints?.[blueprintId]?.title; return ( @@ -62,7 +61,7 @@ export default function AutomationsListItem(props: AutomationsListItemProps) { {cycles.find((cycle) => cycle.value === trigger)?.label} - {blueprintTitle} + {automations?.[automationId]?.title} { it('should return undefined if there are no duplicates', () => { - const automations: Automation[] = [ - { id: '1', title: 'First', trigger: TimerLifeCycle.onClock, blueprintId: '1' }, - { id: '2', title: 'Second', trigger: TimerLifeCycle.onDanger, blueprintId: '2' }, - { id: '3', title: 'Third', trigger: TimerLifeCycle.onLoad, blueprintId: '3' }, + const triggers: Trigger[] = [ + { id: '1', title: 'First', trigger: TimerLifeCycle.onClock, automationId: '1' }, + { id: '2', title: 'Second', trigger: TimerLifeCycle.onDanger, automationId: '2' }, + { id: '3', title: 'Third', trigger: TimerLifeCycle.onLoad, automationId: '3' }, ]; - expect(checkDuplicates(automations)).toBeUndefined(); + expect(checkDuplicates(triggers)).toBeUndefined(); }); it('should return list of titles of duplicates', () => { - const automations: Automation[] = [ - { id: '1', title: 'First', trigger: TimerLifeCycle.onClock, blueprintId: '1' }, - { id: '2', title: 'Second', trigger: TimerLifeCycle.onDanger, blueprintId: '2' }, - { id: '3', title: 'Third', trigger: TimerLifeCycle.onClock, blueprintId: '1' }, - { id: '3', title: 'Third', trigger: TimerLifeCycle.onPause, blueprintId: '1' }, + const triggers: Trigger[] = [ + { id: '1', title: 'First', trigger: TimerLifeCycle.onClock, automationId: '1' }, + { id: '2', title: 'Second', trigger: TimerLifeCycle.onDanger, automationId: '2' }, + { id: '3', title: 'Third', trigger: TimerLifeCycle.onClock, automationId: '1' }, + { id: '3', title: 'Third', trigger: TimerLifeCycle.onPause, automationId: '1' }, ]; - expect(checkDuplicates(automations)).toStrictEqual([2]); + expect(checkDuplicates(triggers)).toStrictEqual([2]); }); }); 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 a002b606a0..3c6435bac2 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 @@ -1,10 +1,10 @@ import { Automation, - AutomationBlueprint, - AutomationBlueprintDTO, + AutomationDTO, CustomFields, OntimeEvent, TimerLifeCycle, + Trigger, } from 'ontime-types'; type CycleLabel = { @@ -26,11 +26,11 @@ export const cycles: CycleLabel[] = [ ]; /** - * We use this guard to find out if the form is receiving an existing blueprint or creating a DTO + * We use this guard to find out if the form is receiving an existing automation or creating a DTO * We do this by checking whether an ID has been generated */ -export function isBlueprint(blueprint: AutomationBlueprintDTO | AutomationBlueprint): blueprint is AutomationBlueprint { - return Object.hasOwn(blueprint, 'id'); +export function isAutomation(automation: AutomationDTO | Automation): automation is Automation { + return Object.hasOwn(automation, 'id'); } export const staticSelectProperties = [ @@ -61,22 +61,22 @@ export function makeFieldList(customFields: CustomFields): SelectableField[] { } /** - * We warn the user if they have created multiple links between the same blueprint and automation + * We warn the user if they have created multiple links between the same automation and a trigger */ -export function checkDuplicates(automations: Automation[]) { - const automationMap: Record = {}; +export function checkDuplicates(triggers: Trigger[]) { + const triggersMap: Record = {}; const duplicates = []; - for (let i = 0; i < automations.length; i++) { - const automation = automations[i]; - if (!Object.hasOwn(automationMap, automation.trigger)) { - automationMap[automation.trigger] = []; + for (let i = 0; i < triggers.length; i++) { + const trigger = triggers[i]; + if (!Object.hasOwn(triggersMap, trigger.trigger)) { + triggersMap[trigger.trigger] = []; } - if (automationMap[automation.trigger].includes(automation.blueprintId)) { + if (triggersMap[trigger.trigger].includes(trigger.automationId)) { duplicates.push(i); } else { - automationMap[automation.trigger].push(automation.blueprintId); + triggersMap[trigger.trigger].push(trigger.automationId); } } return duplicates.length > 0 ? duplicates : undefined; diff --git a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx index 75350d65ff..0455495409 100644 --- a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx +++ b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx @@ -51,8 +51,8 @@ const staticOptions = [ label: 'Automation', secondary: [ { id: 'automation__settings', label: 'Automation settings' }, + { id: 'automation__triggers', label: 'Manage triggers' }, { id: 'automation__automations', label: 'Manage automations' }, - { id: 'automation__blueprints', label: 'Manage blueprints' }, ], }, { diff --git a/apps/server/src/api-data/automation/__tests__/automation.dao.test.ts b/apps/server/src/api-data/automation/__tests__/automation.dao.test.ts index 1f9c29798e..0c12f20ad0 100644 --- a/apps/server/src/api-data/automation/__tests__/automation.dao.test.ts +++ b/apps/server/src/api-data/automation/__tests__/automation.dao.test.ts @@ -1,16 +1,16 @@ -import { AutomationBlueprint, AutomationBlueprintDTO, AutomationDTO, TimerLifeCycle } from 'ontime-types'; +import { TriggerDTO, TimerLifeCycle, AutomationDTO, Automation } from 'ontime-types'; import { + addTrigger, addAutomation, - addBlueprint, deleteAll, - deleteAllAutomations, + deleteAllTriggers, + deleteTrigger, deleteAutomation, - deleteBlueprint, + editTrigger, editAutomation, - editBlueprint, + getAutomationTriggers, getAutomations, - getBlueprints, } from '../automation.dao.js'; import { makeOSCAction, makeHTTPAction } from './testUtils.js'; @@ -21,8 +21,8 @@ beforeAll(() => { enabledAutomations: true, enabledOscIn: true, oscPortIn: 8888, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }; return { getDataProvider: vi.fn().mockImplementation(() => { @@ -39,117 +39,117 @@ afterAll(() => { vi.clearAllMocks(); }); -describe('addAutomations()', () => { +describe('addTrigger()', () => { beforeEach(() => { - deleteAllAutomations(); + deleteAllTriggers(); }); it('should accept a valid automation', () => { - const testData: AutomationDTO = { + const testData: TriggerDTO = { title: 'test', trigger: TimerLifeCycle.onLoad, - blueprintId: 'test-blueprint-id', + automationId: 'test-automation-id', }; - const automation = addAutomation(testData); - expect(automation).toMatchObject(testData); + const trigger = addTrigger(testData); + expect(trigger).toMatchObject(testData); }); }); -describe('editAutomation()', () => { +describe('editTrigger()', () => { beforeEach(() => { - deleteAllAutomations(); - addAutomation({ + deleteAllTriggers(); + addTrigger({ title: 'test-osc', trigger: TimerLifeCycle.onLoad, - blueprintId: 'test-osc-blueprint', + automationId: 'test-osc-automation', }); - addAutomation({ + addTrigger({ title: 'test-http', trigger: TimerLifeCycle.onFinish, - blueprintId: 'test-http-blueprint', + automationId: 'test-http-automation', }); }); it('should edit the contents of an automation', () => { - const automations = getAutomations(); - const firstAutomation = automations[0]; - expect(firstAutomation).toMatchObject({ id: expect.any(String), title: 'test-osc' }); + const triggers = getAutomationTriggers(); + const fistTrigger = triggers[0]; + expect(fistTrigger).toMatchObject({ id: expect.any(String), title: 'test-osc' }); - const editedOSC = editAutomation(firstAutomation.id, { + const editedOSC = editTrigger(fistTrigger.id, { title: 'edited-title', trigger: TimerLifeCycle.onDanger, - blueprintId: 'test-osc-blueprint', + automationId: 'test-osc-automation', }); expect(editedOSC).toMatchObject({ id: expect.any(String), title: 'edited-title', trigger: TimerLifeCycle.onDanger, - blueprintId: 'test-osc-blueprint', + automationId: 'test-osc-automation', }); }); }); -describe('deleteAutomation()', () => { +describe('deleteTrigger()', () => { beforeEach(() => { - deleteAllAutomations(); - addAutomation({ + deleteAllTriggers(); + addTrigger({ title: 'test-osc', trigger: TimerLifeCycle.onLoad, - blueprintId: 'test-osc-blueprint', + automationId: 'test-osc-automation', }); - addAutomation({ + addTrigger({ title: 'test-http', trigger: TimerLifeCycle.onFinish, - blueprintId: 'test-http-blueprint', + automationId: 'test-http-automation', }); }); it('should remove an automation from the list', () => { - const automations = getAutomations(); - expect(automations.length).toEqual(2); - const firstAutomation = automations[0]; - expect(firstAutomation).toMatchObject({ id: expect.any(String), title: 'test-osc' }); + const triggers = getAutomationTriggers(); + expect(triggers.length).toEqual(2); + const fistTrigger = triggers[0]; + expect(fistTrigger).toMatchObject({ id: expect.any(String), title: 'test-osc' }); - deleteAutomation(firstAutomation.id); - const removed = getAutomations(); + deleteTrigger(fistTrigger.id); + const removed = getAutomationTriggers(); expect(removed.length).toEqual(1); expect(removed[0].title).not.toEqual('test-osc'); }); }); -describe('addBlueprint()', () => { +describe('addAutomation()', () => { beforeEach(() => { deleteAll(); }); - it('should accept a valid blueprint', () => { - const testData: AutomationBlueprintDTO = { + it('should accept a valid automation', () => { + const testData: AutomationDTO = { title: 'test', filterRule: 'all', filters: [], outputs: [makeOSCAction(), makeHTTPAction()], }; - const blueprint = addBlueprint(testData); - const blueprints = getBlueprints(); - expect(blueprints[blueprint.id]).toMatchObject(testData); + const automation = addAutomation(testData); + const automations = getAutomations(); + expect(automations[automation.id]).toMatchObject(testData); }); }); -describe('editBlueprint()', () => { - // saving the ID of the added blueprint - let firstBlueprint: AutomationBlueprint; +describe('editAutomation()', () => { + // saving the ID of the added automation + let firstAutomation: Automation; beforeEach(() => { deleteAll(); - firstBlueprint = addBlueprint({ + firstAutomation = addAutomation({ title: 'test-osc', filterRule: 'all', filters: [], outputs: [], }); - addBlueprint({ + addAutomation({ title: 'test-http', filterRule: 'all', filters: [], @@ -157,18 +157,18 @@ describe('editBlueprint()', () => { }); }); - it('should edit the contents of a blueprint', () => { - const blueprints = getBlueprints(); - expect(Object.keys(blueprints).length).toEqual(2); - expect(blueprints[firstBlueprint.id]).toMatchObject({ - id: firstBlueprint.id, + it('should edit the contents of an automation', () => { + const automations = getAutomations(); + expect(Object.keys(automations).length).toEqual(2); + expect(automations[firstAutomation.id]).toMatchObject({ + id: firstAutomation.id, title: 'test-osc', filterRule: 'all', filters: expect.any(Array), outputs: expect.any(Array), }); - const editedOSC = editBlueprint(firstBlueprint.id, { + const editedOSC = editAutomation(firstAutomation.id, { title: 'edited-title', filterRule: 'any', filters: [], @@ -176,7 +176,7 @@ describe('editBlueprint()', () => { }); expect(editedOSC).toMatchObject({ - id: firstBlueprint.id, + id: firstAutomation.id, title: 'edited-title', filterRule: 'any', filters: expect.any(Array), @@ -185,12 +185,12 @@ describe('editBlueprint()', () => { }); }); -describe('deleteBlueprint()', () => { - // saving the ID of the added blueprint - let firstBlueprint: AutomationBlueprint; +describe('deleteAutomation()', () => { + // saving the ID of the added automation + let firstAutomation: Automation; beforeEach(() => { deleteAll(); - firstBlueprint = addBlueprint({ + firstAutomation = addAutomation({ title: 'test-osc', filterRule: 'all', filters: [], @@ -198,35 +198,35 @@ describe('deleteBlueprint()', () => { }); }); - it('should remove a blueprint from the list', () => { - const blueprints = getBlueprints(); - expect(Object.keys(blueprints).length).toEqual(1); + it('should remove m automation from the list', () => { + const automations = getAutomations(); + expect(Object.keys(automations).length).toEqual(1); - deleteBlueprint(Object.keys(blueprints)[0]); - const removed = getBlueprints(); + deleteAutomation(Object.keys(automations)[0]); + const removed = getAutomations(); expect(Object.keys(removed).length).toEqual(0); }); - it('should not remove a blueprint which is in use', () => { - const blueprints = getBlueprints(); - addAutomation({ + it('should not remove an automation which is in use', () => { + const automations = getAutomations(); + addTrigger({ title: 'test-automation', trigger: TimerLifeCycle.onLoad, - blueprintId: firstBlueprint.id, + automationId: firstAutomation.id, }); - const blueprintKeys = Object.keys(blueprints); - const blueprintId = blueprintKeys[0]; - expect(blueprintId).toEqual(firstBlueprint.id); - expect(blueprintKeys.length).toEqual(1); - expect(blueprints[blueprintId]).toMatchObject({ - id: blueprintId, + const automationKeys = Object.keys(automations); + const automationId = automationKeys[0]; + expect(automationId).toEqual(firstAutomation.id); + expect(automationKeys.length).toEqual(1); + expect(automations[automationId]).toMatchObject({ + id: automationId, title: 'test-osc', filterRule: 'all', filters: expect.any(Array), outputs: expect.any(Array), }); - expect(() => deleteBlueprint(blueprintId)).toThrowError(); + expect(() => deleteAutomation(automationId)).toThrowError(); }); }); diff --git a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts index ec2a5c08bd..418fcf5ce9 100644 --- a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts +++ b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts @@ -3,7 +3,7 @@ import { PlayableEvent, TimerLifeCycle } from 'ontime-types'; import { makeRuntimeStateData } from '../../../stores/__mocks__/runtimeState.mocks.js'; import { makeOntimeEvent } from '../../../services/rundown-service/__mocks__/rundown.mocks.js'; -import { deleteAllAutomations, addAutomation, addBlueprint } from '../automation.dao.js'; +import { deleteAllTriggers, addTrigger, addAutomation } from '../automation.dao.js'; import { testConditions, triggerAutomations } from '../automation.service.js'; import * as oscClient from '../clients/osc.client.js'; import * as httpClient from '../clients/http.client.js'; @@ -17,8 +17,8 @@ beforeAll(() => { enabledAutomations: true, enabledOscIn: true, oscPortIn: 8888, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }; return { getDataProvider: vi.fn().mockImplementation(() => { @@ -43,28 +43,28 @@ describe('triggerAction()', () => { oscSpy = vi.spyOn(oscClient, 'emitOSC').mockImplementation(() => {}); httpSpy = vi.spyOn(httpClient, 'emitHTTP').mockImplementation(() => {}); - deleteAllAutomations(); - const oscBlueprint = addBlueprint({ + deleteAllTriggers(); + const oscAutomation = addAutomation({ title: 'test-osc', filterRule: 'all', filters: [], outputs: [makeOSCAction()], }); - const httpBlueprint = addBlueprint({ + const httpAutomation = addAutomation({ title: 'test-http', filterRule: 'any', filters: [], outputs: [makeHTTPAction()], }); - addAutomation({ + addTrigger({ title: 'test-osc', trigger: TimerLifeCycle.onLoad, - blueprintId: oscBlueprint.id, + automationId: oscAutomation.id, }); - addAutomation({ + addTrigger({ title: 'test-http', trigger: TimerLifeCycle.onFinish, - blueprintId: httpBlueprint.id, + automationId: httpAutomation.id, }); }); diff --git a/apps/server/src/api-data/automation/automation.controller.ts b/apps/server/src/api-data/automation/automation.controller.ts index 1edec02fa4..fe1c9f3678 100644 --- a/apps/server/src/api-data/automation/automation.controller.ts +++ b/apps/server/src/api-data/automation/automation.controller.ts @@ -1,5 +1,5 @@ import { getErrorMessage } from 'ontime-utils'; -import { Automation, AutomationBlueprint, AutomationOutput, AutomationSettings, ErrorResponse } from 'ontime-types'; +import { Automation, AutomationOutput, AutomationSettings, ErrorResponse, Trigger } from 'ontime-types'; import type { Request, Response } from 'express'; @@ -18,8 +18,8 @@ export function postAutomationSettings(req: Request, res: Response) { +export function postTrigger(req: Request, res: Response) { try { - const automation = automationDao.addAutomation({ + const automation = automationDao.addTrigger({ title: req.body.title, trigger: req.body.trigger, - blueprintId: req.body.blueprintId, + automationId: req.body.automationId, }); res.status(201).send(automation); } catch (error) { @@ -47,13 +47,13 @@ export function postAutomation(req: Request, res: Response) { +export function putTrigger(req: Request, res: Response) { try { // body payload is a patch object - const automation = automationDao.editAutomation(req.params.id, { + const automation = automationDao.editTrigger(req.params.id, { title: req.body.title ?? undefined, trigger: req.body.trigger ?? undefined, - blueprintId: req.body.blueprintId ?? undefined, + automationId: req.body.automationId ?? undefined, }); res.status(200).send(automation); } catch (error) { @@ -62,9 +62,9 @@ export function putAutomation(req: Request, res: Response) { +export function deleteTrigger(req: Request, res: Response) { try { - automationDao.deleteAutomation(req.params.id); + automationDao.deleteTrigger(req.params.id); res.status(204).send(); } catch (error) { const message = getErrorMessage(error); @@ -72,39 +72,39 @@ export function deleteAutomation(req: Request, res: Response) { +export function postAutomation(req: Request, res: Response) { try { - const newBlueprint = automationDao.addBlueprint({ + const newAutomation = automationDao.addAutomation({ title: req.body.title, filterRule: req.body.filterRule, filters: req.body.filters, outputs: req.body.outputs, }); - res.status(201).send(newBlueprint); + res.status(201).send(newAutomation); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } } -export function editBlueprint(req: Request, res: Response) { +export function editAutomation(req: Request, res: Response) { try { - const newBlueprint = automationDao.editBlueprint(req.params.id, { + const newAutomation = automationDao.editAutomation(req.params.id, { title: req.body.title, filterRule: req.body.filterRule, filters: req.body.filters, outputs: req.body.outputs, }); - res.status(200).send(newBlueprint); + res.status(200).send(newAutomation); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } } -export function deleteBlueprint(req: Request, res: Response) { +export function deleteAutomation(req: Request, res: Response) { try { - automationDao.deleteBlueprint(req.params.id); + automationDao.deleteAutomation(req.params.id); res.status(204).send(); } catch (error) { const message = getErrorMessage(error); diff --git a/apps/server/src/api-data/automation/automation.dao.ts b/apps/server/src/api-data/automation/automation.dao.ts index 815c58b9ce..9a8ba2fb2f 100644 --- a/apps/server/src/api-data/automation/automation.dao.ts +++ b/apps/server/src/api-data/automation/automation.dao.ts @@ -1,11 +1,4 @@ -import type { - Automation, - AutomationBlueprint, - AutomationBlueprintDTO, - AutomationDTO, - AutomationSettings, - NormalisedAutomationBlueprint, -} from 'ontime-types'; +import type { Automation, AutomationDTO, AutomationSettings, NormalisedAutomation, Trigger, TriggerDTO } from 'ontime-types'; import { deleteAtIndex, generateId } from 'ontime-utils'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; @@ -25,19 +18,22 @@ export function getAutomationsEnabled(): boolean { } /** - * Gets a copy of the stored automations + * Gets a copy of the stored automation triggers */ -export function getAutomations(): Automation[] { - return getAutomationSettings().automations; +export function getAutomationTriggers(): Trigger[] { + return getAutomationSettings().triggers; } /** - * Gets a copy of the stored blueprints + * Gets a copy of the stored automations */ -export function getBlueprints(): NormalisedAutomationBlueprint { - return getAutomationSettings().blueprints; +export function getAutomations(): NormalisedAutomation { + return getAutomationSettings().automations; } +/** + * Patches the automation settings object + */ export function editAutomationSettings(settings: Partial): AutomationSettings { saveChanges(settings); return getAutomationSettings(); @@ -46,105 +42,105 @@ export function editAutomationSettings(settings: Partial): A /** * Adds a validated automation to the store */ -export function addAutomation(newAutomation: AutomationDTO): Automation { - const automations = getAutomations(); - const id = getUniqueAutomationId(automations); - const automation = { ...newAutomation, id }; - automations.push(automation); - saveChanges({ automations }); - return automation; +export function addTrigger(newTrigger: TriggerDTO): Trigger { + const triggers = getAutomationTriggers(); + const id = getUniqueTriggerId(triggers); + const trigger = { ...newTrigger, id }; + triggers.push(trigger); + saveChanges({ triggers }); + return trigger; } /** - * Patches an existing automation + * Patches an existing automation trigger */ -export function editAutomation(id: string, newAutomation: AutomationDTO): Automation { - const automations = getAutomations(); - const index = automations.findIndex((automation) => automation.id === id); +export function editTrigger(id: string, newTrigger: TriggerDTO): Trigger { + const triggers = getAutomationTriggers(); + const index = triggers.findIndex((trigger) => trigger.id === id); if (index === -1) { throw new Error(`Automation with id ${id} not found`); } - automations[index] = { ...automations[index], ...newAutomation }; - saveChanges({ automations }); - return automations[index]; + triggers[index] = { ...triggers[index], ...newTrigger }; + saveChanges({ triggers }); + return triggers[index]; } /** - * Deletes an automation given its ID + * Deletes an automation trigger given its ID */ -export function deleteAutomation(id: string): void { - let automations = getAutomations(); - const index = automations.findIndex((automation) => automation.id === id); +export function deleteTrigger(id: string): void { + let triggers = getAutomationTriggers(); + const index = triggers.findIndex((trigger) => trigger.id === id); if (index === -1) { throw new Error(`Automation with id ${id} not found`); } - automations = deleteAtIndex(index, automations); - saveChanges({ automations }); + triggers = deleteAtIndex(index, triggers); + saveChanges({ triggers }); } /** - * Deletes all project automations + * Deletes all project automation triggers */ -export function deleteAllAutomations(): void { - saveChanges({ automations: [] }); +export function deleteAllTriggers(): void { + saveChanges({ triggers: [] }); } /** - * Deletes all project automations and blueprints + * Deletes all project automation triggers and automations * We do this together to avoid issues with missing references */ export function deleteAll(): void { - saveChanges({ automations: [], blueprints: {} }); + saveChanges({ triggers: [], automations: {} }); } /** - * Adds a validated blueprint to the store + * Adds a validated automation to the store */ -export function addBlueprint(newBlueprint: Omit): AutomationBlueprint { - const blueprints = getBlueprints(); - const id = getUniqueBlueprintId(blueprints); - blueprints[id] = { ...newBlueprint, id }; - saveChanges({ blueprints }); - return blueprints[id]; +export function addAutomation(newAutomation: AutomationDTO): Automation { + const automations = getAutomations(); + const id = getUniqueAutomationId(automations); + automations[id] = { ...newAutomation, id }; + saveChanges({ automations }); + return automations[id]; } /** - * Updates an existing blueprint with a new entry + * Updates an existing automation with a new entry */ -export function editBlueprint(id: string, newBlueprint: AutomationBlueprintDTO): AutomationBlueprint { - const blueprints = getBlueprints(); - if (!Object.hasOwn(blueprints, id)) { - throw new Error(`Blueprint with id ${id} not found`); +export function editAutomation(id: string, newAutomation: AutomationDTO): Automation { + const automations = getAutomations(); + if (!Object.hasOwn(automations, id)) { + throw new Error(`Automation with id ${id} not found`); } - blueprints[id] = { ...newBlueprint, id }; - saveChanges({ blueprints }); - return blueprints[id]; + automations[id] = { ...newAutomation, id }; + saveChanges({ automations }); + return automations[id]; } /** - * Deletes a blueprint given its ID + * Deletes a automation given its ID */ -export function deleteBlueprint(id: string): void { - const blueprints = getBlueprints(); - // ignore request if blueprint does not exist - if (!Object.hasOwn(blueprints, id)) { +export function deleteAutomation(id: string): void { + const automations = getAutomations(); + // ignore request if automation does not exist + if (!Object.hasOwn(automations, id)) { return; } - // prevent deleting a blueprint that is in use - const automations = getAutomations(); - for (let i = 0; i < automations.length; i++) { - const automation = automations[i]; - if (automation.blueprintId === id) { - throw new Error(`Unable to delete blueprint used in automation ${automation.title}`); + // prevent deleting a automation that is in use + const triggers = getAutomationTriggers(); + for (let i = 0; i < triggers.length; i++) { + const trigger = triggers[i]; + if (trigger.automationId === id) { + throw new Error(`Unable to delete automation used in trigger ${trigger.title}`); } } - delete blueprints[id]; - saveChanges({ blueprints }); + delete automations[id]; + saveChanges({ automations }); } /** @@ -161,14 +157,14 @@ async function saveChanges(patch: Partial) { /** * Returns an ID guaranteed to be unique in an array */ -function getUniqueAutomationId(automations: Automation[]): string { +function getUniqueTriggerId(triggers: Trigger[]): string { let id = ''; do { id = generateId(); } while (isInArray(id)); function isInArray(id: string): boolean { - return automations.some((automation) => automation.id === id); + return triggers.some((trigger) => trigger.id === id); } return id; } @@ -176,10 +172,10 @@ function getUniqueAutomationId(automations: Automation[]): string { /** * Returns an ID guaranteed to be unique in an objects keys */ -function getUniqueBlueprintId(blueprints: NormalisedAutomationBlueprint): string { +function getUniqueAutomationId(automations: NormalisedAutomation): string { let id = ''; do { id = generateId(); - } while (Object.hasOwn(blueprints, id)); + } while (Object.hasOwn(automations, id)); return id; } diff --git a/apps/server/src/api-data/automation/automation.parser.ts b/apps/server/src/api-data/automation/automation.parser.ts index 35653c1e99..27fb635c58 100644 --- a/apps/server/src/api-data/automation/automation.parser.ts +++ b/apps/server/src/api-data/automation/automation.parser.ts @@ -1,4 +1,4 @@ -import { DatabaseModel, AutomationSettings, Automation, NormalisedAutomationBlueprint } from 'ontime-types'; +import { DatabaseModel, AutomationSettings, NormalisedAutomation, Trigger } from 'ontime-types'; import { dbModel } from '../../models/dataModel.js'; import type { ErrorEmitter } from '../../utils/parser.js'; @@ -24,8 +24,8 @@ export function parseAutomationSettings(data: LegacyData, emitError?: ErrorEmitt enabledAutomations: dbModel.automation.enabledAutomations, enabledOscIn: data.osc?.enabledIn ?? dbModel.automation.enabledOscIn, oscPortIn: data.osc?.portIn ?? dbModel.automation.oscPortIn, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }; } else { return { ...dbModel.automation }; @@ -42,17 +42,17 @@ export function parseAutomationSettings(data: LegacyData, emitError?: ErrorEmitt enabledAutomations: data.automation.enabledAutomations ?? dbModel.automation.enabledAutomations, enabledOscIn: data.automation.enabledOscIn ?? dbModel.automation.enabledOscIn, oscPortIn: data.automation.oscPortIn ?? dbModel.automation.oscPortIn, + triggers: parseTriggers(data.automation.triggers), automations: parseAutomations(data.automation.automations), - blueprints: parseBlueprints(data.automation.blueprints), }; } -function parseAutomations(maybeAutomations: unknown): Automation[] { +function parseTriggers(maybeAutomations: unknown): Trigger[] { if (!Array.isArray(maybeAutomations)) return []; - return maybeAutomations as Automation[]; + return maybeAutomations as Trigger[]; } -function parseBlueprints(maybeBlueprint: unknown): NormalisedAutomationBlueprint { - if (typeof maybeBlueprint !== 'object' || maybeBlueprint === null) return {}; - return maybeBlueprint as NormalisedAutomationBlueprint; +function parseAutomations(maybeAutomation: unknown): NormalisedAutomation { + if (typeof maybeAutomation !== 'object' || maybeAutomation === null) return {}; + return maybeAutomation as NormalisedAutomation; } diff --git a/apps/server/src/api-data/automation/automation.router.ts b/apps/server/src/api-data/automation/automation.router.ts index 94ffe58a52..0a6fb0234a 100644 --- a/apps/server/src/api-data/automation/automation.router.ts +++ b/apps/server/src/api-data/automation/automation.router.ts @@ -1,24 +1,24 @@ import express from 'express'; import { + deleteTrigger, deleteAutomation, - deleteBlueprint, - editBlueprint, + editAutomation, getAutomationSettings, + postTrigger, postAutomation, - postBlueprint, - putAutomation, + putTrigger, postAutomationSettings, testOutput, } from './automation.controller.js'; import { paramContainsId, + validateAutomationSettings, validateAutomation, validateAutomationPatch, - validateAutomationSettings, - validateBlueprint, - validateBlueprintPatch, validateTestPayload, + validateTrigger, + validateTriggerPatch, } from './automation.validation.js'; export const router = express.Router(); @@ -26,12 +26,12 @@ export const router = express.Router(); router.get('/', getAutomationSettings); router.post('/', validateAutomationSettings, postAutomationSettings); +router.post('/trigger', validateTrigger, postTrigger); +router.put('/trigger/:id', validateTriggerPatch, putTrigger); +router.delete('/trigger/:id', paramContainsId, deleteTrigger); + router.post('/automation', validateAutomation, postAutomation); -router.put('/automation/:id', validateAutomationPatch, putAutomation); +router.put('/automation/:id', validateAutomationPatch, editAutomation); router.delete('/automation/:id', paramContainsId, deleteAutomation); -router.post('/blueprint', validateBlueprint, postBlueprint); -router.put('/blueprint/:id', validateBlueprintPatch, editBlueprint); -router.delete('/blueprint/:id', paramContainsId, deleteBlueprint); - router.post('/test', validateTestPayload, testOutput); diff --git a/apps/server/src/api-data/automation/automation.service.ts b/apps/server/src/api-data/automation/automation.service.ts index 3fd2b55b3a..371f72e5c2 100644 --- a/apps/server/src/api-data/automation/automation.service.ts +++ b/apps/server/src/api-data/automation/automation.service.ts @@ -13,7 +13,7 @@ import { isOntimeCloud } from '../../externals.js'; import { emitOSC } from './clients/osc.client.js'; import { emitHTTP } from './clients/http.client.js'; -import { getAutomations, getAutomationsEnabled, getBlueprints } from './automation.dao.js'; +import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js'; /** * Exposes a method for triggering actions based on a TimerLifeCycle event @@ -23,25 +23,25 @@ export function triggerAutomations(event: TimerLifeCycle, state: RuntimeState) { return; } - const automations = getAutomations(); - const triggerAutomations = automations.filter((automation) => automation.trigger === event); + const triggers = getAutomationTriggers(); + const triggerAutomations = triggers.filter((trigger) => trigger.trigger === event); if (triggerAutomations.length === 0) { return; } - const blueprints = getBlueprints(); - if (Object.keys(blueprints).length === 0) { + const automations = getAutomations(); + if (Object.keys(automations).length === 0) { return; } - triggerAutomations.forEach((automation) => { - const blueprint = blueprints[automation.blueprintId]; - if (!blueprint) { + triggerAutomations.forEach((trigger) => { + const automation = automations[trigger.automationId]; + if (!automation) { return; } - const shouldSend = testConditions(blueprint.filters, blueprint.filterRule, state); + const shouldSend = testConditions(automation.filters, automation.filterRule, state); if (shouldSend) { - send(blueprint.outputs, state); + send(automation.outputs, state); } }); } diff --git a/apps/server/src/api-data/automation/automation.validation.ts b/apps/server/src/api-data/automation/automation.validation.ts index 2e9796a83b..f29665d98a 100644 --- a/apps/server/src/api-data/automation/automation.validation.ts +++ b/apps/server/src/api-data/automation/automation.validation.ts @@ -1,5 +1,5 @@ import { - AutomationBlueprint, + Automation, AutomationFilter, AutomationOutput, HTTPOutput, @@ -28,11 +28,11 @@ export const validateAutomationSettings = [ body('enabledAutomations').exists().isBoolean(), body('enabledOscIn').exists().isBoolean(), body('oscPortIn').exists().isPort(), - body('automations').optional().isArray(), - body('automations.*.title').optional().isString().trim(), - body('automations.*.trigger').optional().isIn(timerLifecycleValues), - body('automations.*.blueprintId').optional().isString().trim(), - body('blueprints').optional().custom(parseBluePrint), + body('triggers').optional().isArray(), + body('triggers.*.title').optional().isString().trim(), + body('triggers.*.trigger').optional().isIn(timerLifecycleValues), + body('triggers.*.automationId').optional().isString().trim(), + body('automations').optional().custom(parseAutomation), (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); @@ -41,10 +41,10 @@ export const validateAutomationSettings = [ }, ]; -export const validateAutomation = [ +export const validateTrigger = [ body('title').exists().isString().trim(), body('trigger').exists().isIn(timerLifecycleValues), - body('blueprintId').exists().isString().trim(), + body('automationId').exists().isString().trim(), (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); @@ -53,11 +53,11 @@ export const validateAutomation = [ }, ]; -export const validateAutomationPatch = [ +export const validateTriggerPatch = [ param('id').exists(), body('title').optional().isString().trim(), body('trigger').optional().isIn(timerLifecycleValues), - body('blueprintId').optional().isString().trim(), + body('automationId').optional().isString().trim(), (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); @@ -66,8 +66,8 @@ export const validateAutomationPatch = [ }, ]; -export const validateBlueprint = [ - body().custom(parseBluePrint), +export const validateAutomation = [ + body().custom(parseAutomation), (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); @@ -76,9 +76,9 @@ export const validateBlueprint = [ }, ]; -export const validateBlueprintPatch = [ +export const validateAutomationPatch = [ param('id').exists(), - body().custom(parseBluePrint), + body().custom(parseAutomation), (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); @@ -88,17 +88,17 @@ export const validateBlueprintPatch = [ ]; /** - * Parses and validates a use given blueprint + * Parses and validates a use given automation */ -export function parseBluePrint(maybeBlueprint: unknown): AutomationBlueprint { - assert.isObject(maybeBlueprint); - assert.hasKeys(maybeBlueprint, ['title', 'filterRule', 'filters', 'outputs']); +export function parseAutomation(maybeAutomation: unknown): Automation { + assert.isObject(maybeAutomation); + assert.hasKeys(maybeAutomation, ['title', 'filterRule', 'filters', 'outputs']); - const { title, filterRule, filters, outputs } = maybeBlueprint; + const { title, filterRule, filters, outputs } = maybeAutomation; assert.isString(title); assert.isString(filterRule); if (!isFilterRule(filterRule)) { - throw new Error(`Invalid blueprint: unknown filter rule ${filterRule}`); + throw new Error(`Invalid automation: unknown filter rule ${filterRule}`); } assert.isArray(filters); validateFilters(filters); @@ -106,7 +106,7 @@ export function parseBluePrint(maybeBlueprint: unknown): AutomationBlueprint { assert.isArray(outputs); validateOutput(outputs); - return maybeBlueprint as AutomationBlueprint; + return maybeAutomation as Automation; } function validateFilters(filters: Array): filters is AutomationFilter[] { @@ -119,7 +119,7 @@ function validateFilters(filters: Array): filters is AutomationFilter[] assert.isString(operator); assert.isString(value); if (!isFilterOperator(operator)) { - throw new Error(`Invalid blueprint: unknown filter operator ${operator}`); + throw new Error(`Invalid automation: unknown filter operator ${operator}`); } if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { diff --git a/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts b/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts index c616a82177..28314cf30c 100644 --- a/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts +++ b/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts @@ -39,8 +39,8 @@ describe('safeMerge', () => { enabledAutomations: false, enabledOscIn: false, oscPortIn: 8000, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }, } as DatabaseModel; @@ -127,8 +127,8 @@ describe('safeMerge', () => { enabledAutomations: false, enabledOscIn: false, oscPortIn: 8000, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }, } as DatabaseModel; diff --git a/apps/server/src/models/dataModel.ts b/apps/server/src/models/dataModel.ts index ccd6fb03e6..a4a73a437f 100644 --- a/apps/server/src/models/dataModel.ts +++ b/apps/server/src/models/dataModel.ts @@ -35,7 +35,7 @@ export const dbModel: DatabaseModel = { enabledAutomations: true, enabledOscIn: true, oscPortIn: 8888, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }, }; diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index ce3d7c1247..b57e35dd61 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -454,7 +454,7 @@ export const demoDb: DatabaseModel = { enabledAutomations: false, enabledOscIn: true, oscPortIn: 8888, - automations: [], - blueprints: {}, + triggers: [], + automations: {}, }, }; diff --git a/apps/server/test-db/db.json b/apps/server/test-db/db.json index a047adca82..6224541f91 100644 --- a/apps/server/test-db/db.json +++ b/apps/server/test-db/db.json @@ -440,8 +440,8 @@ "enabledAutomations": false, "enabledOscIn": true, "oscPortIn": 8888, - "automations": [], - "blueprints": {} + "triggers": [], + "automations": {} }, "customFields": { "song": { diff --git a/e2e/tests/fixtures/test-db.json b/e2e/tests/fixtures/test-db.json index a047adca82..6224541f91 100644 --- a/e2e/tests/fixtures/test-db.json +++ b/e2e/tests/fixtures/test-db.json @@ -440,8 +440,8 @@ "enabledAutomations": false, "enabledOscIn": true, "oscPortIn": 8888, - "automations": [], - "blueprints": {} + "triggers": [], + "automations": {} }, "customFields": { "song": { diff --git a/packages/types/src/definitions/core/Automation.type.ts b/packages/types/src/definitions/core/Automation.type.ts index e5a355a372..67bc141511 100644 --- a/packages/types/src/definitions/core/Automation.type.ts +++ b/packages/types/src/definitions/core/Automation.type.ts @@ -4,33 +4,33 @@ export type AutomationSettings = { enabledAutomations: boolean; enabledOscIn: boolean; oscPortIn: number; - automations: Automation[]; - blueprints: NormalisedAutomationBlueprint; + triggers: Trigger[]; + automations: NormalisedAutomation; }; -type BlueprintId = string; +type AutomationId = string; export type FilterRule = 'all' | 'any'; -export type AutomationBlueprint = { - id: BlueprintId; +export type Automation = { + id: AutomationId; title: string; filterRule: FilterRule; filters: AutomationFilter[]; outputs: AutomationOutput[]; }; -export type AutomationBlueprintDTO = Omit; +export type AutomationDTO = Omit; -export type NormalisedAutomationBlueprint = Record; +export type NormalisedAutomation = Record; -export type Automation = { +export type Trigger = { id: string; title: string; trigger: TimerLifeCycle; - blueprintId: BlueprintId; + automationId: AutomationId; }; -export type AutomationDTO = Omit; +export type TriggerDTO = Omit; export type AutomationFilter = { field: string; // this should be a key of a OntimeEvent + custom fields diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b84273b9e5..6bb4326c8f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -18,17 +18,17 @@ export { TimerType } from './definitions/TimerType.type.js'; // ---> Automations export type { - AutomationSettings, - AutomationBlueprint, - AutomationBlueprintDTO, Automation, AutomationDTO, AutomationFilter, + AutomationSettings, AutomationOutput, FilterRule, HTTPOutput, - NormalisedAutomationBlueprint, + NormalisedAutomation, OSCOutput, + Trigger, + TriggerDTO, } from './definitions/core/Automation.type.js'; // ---> Project Data