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 05999234d..630957c22 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 @@ -107,16 +107,16 @@ export default function AutomationsList(props: AutomationsListProps) { - {deleteError && ( - - - {deleteError} - - - )} ); })} + {deleteError && ( + + + {deleteError} + + + )} 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 d5e497be5..bacdf5db2 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,6 +1,6 @@ -import { TriggerDTO, TimerLifeCycle, AutomationDTO, Automation, EntryId } from 'ontime-types'; +import { TriggerDTO, TimerLifeCycle, AutomationDTO, Automation, ProjectRundowns } from 'ontime-types'; -import { makeRundown } from '../../rundown/__mocks__/rundown.mocks.js'; +import { makeOntimeEvent } from '../../rundown/__mocks__/rundown.mocks.js'; import { addTrigger, @@ -203,10 +203,29 @@ describe('deleteAutomation()', () => { const automations = getAutomations(); expect(Object.keys(automations).length).toEqual(1); - const rundown = makeRundown({}); - const timedEventOrder: EntryId[] = []; - - await deleteAutomation(rundown, timedEventOrder, Object.keys(automations)[0]); + const projectRundowns: ProjectRundowns = { + 'rundown-1': { + id: 'rundown-1', + title: 'Rundown 1', + order: ['1'], + flatOrder: ['1'], + entries: { + '1': makeOntimeEvent({ + id: '1', + triggers: [ + { + id: 'trigger-1', + title: 'Trigger 1', + trigger: TimerLifeCycle.onClock, + automationId: 'test-automation', + }, + ], + }), + }, + revision: 1, + }, + }; + await deleteAutomation(projectRundowns, Object.keys(automations)[0]); const removed = getAutomations(); expect(Object.keys(removed).length).toEqual(0); }); diff --git a/apps/server/src/api-data/automation/__tests__/automation.utils.test.ts b/apps/server/src/api-data/automation/__tests__/automation.utils.test.ts index 7dc03a253..e6e9f6c14 100644 --- a/apps/server/src/api-data/automation/__tests__/automation.utils.test.ts +++ b/apps/server/src/api-data/automation/__tests__/automation.utils.test.ts @@ -1,5 +1,7 @@ -import { TimerLifeCycle } from 'ontime-types'; -import { makeOntimeEvent, makeRundown } from '../../rundown/__mocks__/rundown.mocks.js'; +import { ProjectRundowns, TimerLifeCycle } from 'ontime-types'; + +import { makeOntimeEvent } from '../../rundown/__mocks__/rundown.mocks.js'; + import { isAutomationUsed, parseTemplateNested, stringToOSCArgs } from '../automation.utils.js'; describe('parseTemplateNested()', () => { @@ -250,50 +252,109 @@ describe('test stringToOSCArgs()', () => { describe('isAutomationUsed()', () => { it('returns the first event which uses an automation', () => { - const rundown = makeRundown({ - entries: { - '1': makeOntimeEvent({ - id: '1', - triggers: [ - { - id: 'trigger-1', - title: 'Trigger 1', - trigger: TimerLifeCycle.onClock, - automationId: 'test-automation', - }, - ], - }), + const projectRundowns: ProjectRundowns = { + 'rundown-1': { + id: 'rundown-1', + title: 'Rundown 1', + order: ['1'], + flatOrder: ['1'], + entries: { + '1': makeOntimeEvent({ + id: '1', + triggers: [ + { + id: 'trigger-1', + title: 'Trigger 1', + trigger: TimerLifeCycle.onClock, + automationId: 'test-automation', + }, + ], + }), + }, + revision: 1, }, - }); - - const timedEventOrder = ['1']; + }; const automationId = 'test-automation'; - const result = isAutomationUsed(rundown, timedEventOrder, automationId); - expect(result).toBe('1'); + const result = isAutomationUsed(projectRundowns, automationId); + expect(result).toStrictEqual(['Rundown 1', '1']); + }); + + it('finds usages in any rundown', () => { + const projectRundowns: ProjectRundowns = { + 'rundown-1': { + id: 'rundown-1', + title: 'Rundown 1', + order: ['1'], + flatOrder: ['1'], + entries: { + '1': makeOntimeEvent({ + id: '1', + triggers: [ + { + id: 'trigger-1', + title: 'Trigger 1', + trigger: TimerLifeCycle.onClock, + automationId: 'test-automation', + }, + ], + }), + }, + revision: 1, + }, + 'rundown-2': { + id: 'rundown-2', + title: 'Rundown 2', + order: ['1'], + flatOrder: ['1'], + entries: { + '1': makeOntimeEvent({ + id: '1', + triggers: [ + { + id: 'trigger-1', + title: 'Trigger 1', + trigger: TimerLifeCycle.onClock, + automationId: 'in-the-second-rundown', + }, + ], + }), + }, + revision: 1, + }, + }; + const automationId = 'in-the-second-rundown'; + + const result = isAutomationUsed(projectRundowns, automationId); + expect(result).toStrictEqual(['Rundown 2', '1']); }); it('returns returns undefined if there are no matches', () => { - const rundown = makeRundown({ - entries: { - '1': makeOntimeEvent({ - id: '1', - triggers: [ - { - id: 'trigger-1', - title: 'Trigger 1', - trigger: TimerLifeCycle.onClock, - automationId: 'test-automation', - }, - ], - }), + const projectRundowns: ProjectRundowns = { + 'rundown-1': { + id: 'rundown-1', + title: 'Rundown 1', + order: ['1'], + flatOrder: ['1'], + entries: { + '1': makeOntimeEvent({ + id: '1', + triggers: [ + { + id: 'trigger-1', + title: 'Trigger 1', + trigger: TimerLifeCycle.onClock, + automationId: 'test-automation', + }, + ], + }), + }, + revision: 1, }, - }); - - const timedEventOrder = ['1']; + }; const automationId = 'does-not-exist'; - const result = isAutomationUsed(rundown, timedEventOrder, automationId); + const result = isAutomationUsed(projectRundowns, automationId); expect(result).toBeUndefined(); }); }); diff --git a/apps/server/src/api-data/automation/automation.controller.ts b/apps/server/src/api-data/automation/automation.controller.ts index cc797dee7..9e4a84691 100644 --- a/apps/server/src/api-data/automation/automation.controller.ts +++ b/apps/server/src/api-data/automation/automation.controller.ts @@ -4,8 +4,7 @@ import { Automation, AutomationSettings, ErrorResponse, Trigger } from 'ontime-t import type { Request, Response } from 'express'; import { oscServer } from '../../adapters/OscAdapter.js'; - -import { getCurrentRundown, getRundownMetadata } from '../rundown/rundown.dao.js'; +import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import * as automationDao from './automation.dao.js'; import * as automationService from './automation.service.js'; @@ -108,10 +107,8 @@ export async function editAutomation(req: Request, res: Response) { try { - const rundown = getCurrentRundown(); - const { timedEventOrder } = getRundownMetadata(); - - await automationDao.deleteAutomation(rundown, timedEventOrder, req.params.id); + const projectRundowns = getDataProvider().getProjectRundowns(); + await automationDao.deleteAutomation(projectRundowns, 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 e882675cc..63bbcd61a 100644 --- a/apps/server/src/api-data/automation/automation.dao.ts +++ b/apps/server/src/api-data/automation/automation.dao.ts @@ -2,9 +2,8 @@ import type { Automation, AutomationDTO, AutomationSettings, - EntryId, NormalisedAutomation, - Rundown, + ProjectRundowns, Trigger, TriggerDTO, } from 'ontime-types'; @@ -136,15 +135,15 @@ export async function editAutomation(id: string, newAutomation: AutomationDTO): /** * Deletes a automation given its ID */ -export async function deleteAutomation(rundown: Rundown, timedEventOrder: EntryId[], id: string): Promise { +export async function deleteAutomation(projectRundowns: ProjectRundowns, automationId: string): Promise { const automations = getAutomations(); // ignore request if automation does not exist - if (!Object.hasOwn(automations, id)) { + if (!Object.hasOwn(automations, automationId)) { return; } // prevent deleting a automation that is in use in triggers - const triggers = getAutomationTriggers().filter((trigger) => trigger.automationId === id); + const triggers = getAutomationTriggers().filter((trigger) => trigger.automationId === automationId); if (triggers.length) { throw new Error( `Unable to delete automation used in trigger ${triggers[0].title}${triggers.length > 1 ? ` and ${triggers.length - 1} more` : ''}`, @@ -152,12 +151,12 @@ export async function deleteAutomation(rundown: Rundown, timedEventOrder: EntryI } // prevent deleting a automation that is in use in events - const isInUse = isAutomationUsed(rundown, timedEventOrder, id); + const isInUse = isAutomationUsed(projectRundowns, automationId); if (isInUse) { - throw new Error(`Unable to delete automation used in event with ID ${isInUse}`); + throw new Error(`Unable to delete automation used in rundown: ${isInUse[0]}, in event with ID: ${isInUse[1]}`); } - delete automations[id]; + delete automations[automationId]; await saveChanges({ automations }); } diff --git a/apps/server/src/api-data/automation/automation.utils.ts b/apps/server/src/api-data/automation/automation.utils.ts index 41856bc8c..357b2138d 100644 --- a/apps/server/src/api-data/automation/automation.utils.ts +++ b/apps/server/src/api-data/automation/automation.utils.ts @@ -1,4 +1,13 @@ -import { EntryId, FilterRule, isOntimeEvent, MaybeNumber, OntimeAction, ontimeActionKeyValues, Rundown } from 'ontime-types'; +import { + EntryId, + FilterRule, + isOntimeEvent, + MaybeNumber, + OntimeAction, + ontimeActionKeyValues, + ProjectRundowns, + RundownEntries, +} from 'ontime-types'; import { millisToString, removeLeadingZero, splitWhitespace, getPropertyFromPath } from 'ontime-utils'; import type { OscArgOrArrayInput, OscArgInput } from 'osc-min'; @@ -198,22 +207,40 @@ export function isBooleanEquals(a: boolean, b: string): boolean { /** * Checks is an automation is used in a rundown - * TODO(v4): this currently only checks the current rundown, we will need to check all rundowns in the future */ -export function isAutomationUsed( - rundown: Rundown, - timedEventOrder: EntryId[], +function isAutomationUsedInRundown( + entries: RundownEntries, + flatOrder: EntryId[], automationId: string, ): EntryId | undefined { - for (let i = 0; i < timedEventOrder.length; i++) { - const eventId = timedEventOrder[i]; - const event = rundown.entries[eventId]; - if (isOntimeEvent(event) && event.triggers) { - for (const trigger of event.triggers) { + for (let i = 0; i < flatOrder.length; i++) { + const eventId = flatOrder[i]; + const entry = entries[eventId]; + + // only ontime events can contain triggers + if (isOntimeEvent(entry) && entry.triggers) { + for (const trigger of entry.triggers) { if (trigger.automationId === automationId) { - return eventId; + return entry.id; } } } } } + +/** + * Checks if an automation is used in any of the project rundowns + */ +export function isAutomationUsed( + projectRundowns: ProjectRundowns, + automationId: string, +): [string, EntryId] | undefined { + for (const rundownId in projectRundowns) { + const rundown = projectRundowns[rundownId]; + const usedInEvent = isAutomationUsedInRundown(rundown.entries, rundown.flatOrder, automationId); + + if (usedInEvent) { + return [rundown.title, usedInEvent]; + } + } +}