mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
fix: check automation usage in all rundowns before allowing delete
This commit is contained in:
committed by
Alex Christoffer Rasmussen
parent
cbd7bce106
commit
bcabc50ad1
@@ -107,16 +107,16 @@ export default function AutomationsList(props: AutomationsListProps) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</tr>
|
</tr>
|
||||||
{deleteError && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={5}>
|
|
||||||
<Panel.Error>{deleteError}</Panel.Error>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{deleteError && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5}>
|
||||||
|
<Panel.Error>{deleteError}</Panel.Error>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Panel.Table>
|
</Panel.Table>
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
addTrigger,
|
addTrigger,
|
||||||
@@ -203,10 +203,29 @@ describe('deleteAutomation()', () => {
|
|||||||
const automations = getAutomations();
|
const automations = getAutomations();
|
||||||
expect(Object.keys(automations).length).toEqual(1);
|
expect(Object.keys(automations).length).toEqual(1);
|
||||||
|
|
||||||
const rundown = makeRundown({});
|
const projectRundowns: ProjectRundowns = {
|
||||||
const timedEventOrder: EntryId[] = [];
|
'rundown-1': {
|
||||||
|
id: 'rundown-1',
|
||||||
await deleteAutomation(rundown, timedEventOrder, Object.keys(automations)[0]);
|
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();
|
const removed = getAutomations();
|
||||||
expect(Object.keys(removed).length).toEqual(0);
|
expect(Object.keys(removed).length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { TimerLifeCycle } from 'ontime-types';
|
import { ProjectRundowns, TimerLifeCycle } from 'ontime-types';
|
||||||
import { makeOntimeEvent, makeRundown } from '../../rundown/__mocks__/rundown.mocks.js';
|
|
||||||
|
import { makeOntimeEvent } from '../../rundown/__mocks__/rundown.mocks.js';
|
||||||
|
|
||||||
import { isAutomationUsed, parseTemplateNested, stringToOSCArgs } from '../automation.utils.js';
|
import { isAutomationUsed, parseTemplateNested, stringToOSCArgs } from '../automation.utils.js';
|
||||||
|
|
||||||
describe('parseTemplateNested()', () => {
|
describe('parseTemplateNested()', () => {
|
||||||
@@ -250,50 +252,109 @@ describe('test stringToOSCArgs()', () => {
|
|||||||
|
|
||||||
describe('isAutomationUsed()', () => {
|
describe('isAutomationUsed()', () => {
|
||||||
it('returns the first event which uses an automation', () => {
|
it('returns the first event which uses an automation', () => {
|
||||||
const rundown = makeRundown({
|
const projectRundowns: ProjectRundowns = {
|
||||||
entries: {
|
'rundown-1': {
|
||||||
'1': makeOntimeEvent({
|
id: 'rundown-1',
|
||||||
id: '1',
|
title: 'Rundown 1',
|
||||||
triggers: [
|
order: ['1'],
|
||||||
{
|
flatOrder: ['1'],
|
||||||
id: 'trigger-1',
|
entries: {
|
||||||
title: 'Trigger 1',
|
'1': makeOntimeEvent({
|
||||||
trigger: TimerLifeCycle.onClock,
|
id: '1',
|
||||||
automationId: 'test-automation',
|
triggers: [
|
||||||
},
|
{
|
||||||
],
|
id: 'trigger-1',
|
||||||
}),
|
title: 'Trigger 1',
|
||||||
|
trigger: TimerLifeCycle.onClock,
|
||||||
|
automationId: 'test-automation',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
revision: 1,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
const timedEventOrder = ['1'];
|
|
||||||
const automationId = 'test-automation';
|
const automationId = 'test-automation';
|
||||||
|
|
||||||
const result = isAutomationUsed(rundown, timedEventOrder, automationId);
|
const result = isAutomationUsed(projectRundowns, automationId);
|
||||||
expect(result).toBe('1');
|
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', () => {
|
it('returns returns undefined if there are no matches', () => {
|
||||||
const rundown = makeRundown({
|
const projectRundowns: ProjectRundowns = {
|
||||||
entries: {
|
'rundown-1': {
|
||||||
'1': makeOntimeEvent({
|
id: 'rundown-1',
|
||||||
id: '1',
|
title: 'Rundown 1',
|
||||||
triggers: [
|
order: ['1'],
|
||||||
{
|
flatOrder: ['1'],
|
||||||
id: 'trigger-1',
|
entries: {
|
||||||
title: 'Trigger 1',
|
'1': makeOntimeEvent({
|
||||||
trigger: TimerLifeCycle.onClock,
|
id: '1',
|
||||||
automationId: 'test-automation',
|
triggers: [
|
||||||
},
|
{
|
||||||
],
|
id: 'trigger-1',
|
||||||
}),
|
title: 'Trigger 1',
|
||||||
|
trigger: TimerLifeCycle.onClock,
|
||||||
|
automationId: 'test-automation',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
revision: 1,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
const timedEventOrder = ['1'];
|
|
||||||
const automationId = 'does-not-exist';
|
const automationId = 'does-not-exist';
|
||||||
|
|
||||||
const result = isAutomationUsed(rundown, timedEventOrder, automationId);
|
const result = isAutomationUsed(projectRundowns, automationId);
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { Automation, AutomationSettings, ErrorResponse, Trigger } from 'ontime-t
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
import { oscServer } from '../../adapters/OscAdapter.js';
|
import { oscServer } from '../../adapters/OscAdapter.js';
|
||||||
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { getCurrentRundown, getRundownMetadata } from '../rundown/rundown.dao.js';
|
|
||||||
|
|
||||||
import * as automationDao from './automation.dao.js';
|
import * as automationDao from './automation.dao.js';
|
||||||
import * as automationService from './automation.service.js';
|
import * as automationService from './automation.service.js';
|
||||||
@@ -108,10 +107,8 @@ export async function editAutomation(req: Request, res: Response<Automation | Er
|
|||||||
|
|
||||||
export async function deleteAutomation(req: Request, res: Response<void | ErrorResponse>) {
|
export async function deleteAutomation(req: Request, res: Response<void | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
const rundown = getCurrentRundown();
|
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||||
const { timedEventOrder } = getRundownMetadata();
|
await automationDao.deleteAutomation(projectRundowns, req.params.id);
|
||||||
|
|
||||||
await automationDao.deleteAutomation(rundown, timedEventOrder, req.params.id);
|
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import type {
|
|||||||
Automation,
|
Automation,
|
||||||
AutomationDTO,
|
AutomationDTO,
|
||||||
AutomationSettings,
|
AutomationSettings,
|
||||||
EntryId,
|
|
||||||
NormalisedAutomation,
|
NormalisedAutomation,
|
||||||
Rundown,
|
ProjectRundowns,
|
||||||
Trigger,
|
Trigger,
|
||||||
TriggerDTO,
|
TriggerDTO,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
@@ -136,15 +135,15 @@ export async function editAutomation(id: string, newAutomation: AutomationDTO):
|
|||||||
/**
|
/**
|
||||||
* Deletes a automation given its ID
|
* Deletes a automation given its ID
|
||||||
*/
|
*/
|
||||||
export async function deleteAutomation(rundown: Rundown, timedEventOrder: EntryId[], id: string): Promise<void> {
|
export async function deleteAutomation(projectRundowns: ProjectRundowns, automationId: string): Promise<void> {
|
||||||
const automations = getAutomations();
|
const automations = getAutomations();
|
||||||
// ignore request if automation does not exist
|
// ignore request if automation does not exist
|
||||||
if (!Object.hasOwn(automations, id)) {
|
if (!Object.hasOwn(automations, automationId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent deleting a automation that is in use in triggers
|
// 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) {
|
if (triggers.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unable to delete automation used in trigger ${triggers[0].title}${triggers.length > 1 ? ` and ${triggers.length - 1} more` : ''}`,
|
`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
|
// prevent deleting a automation that is in use in events
|
||||||
const isInUse = isAutomationUsed(rundown, timedEventOrder, id);
|
const isInUse = isAutomationUsed(projectRundowns, automationId);
|
||||||
if (isInUse) {
|
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 });
|
await saveChanges({ automations });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { millisToString, removeLeadingZero, splitWhitespace, getPropertyFromPath } from 'ontime-utils';
|
||||||
import type { OscArgOrArrayInput, OscArgInput } from 'osc-min';
|
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
|
* 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(
|
function isAutomationUsedInRundown(
|
||||||
rundown: Rundown,
|
entries: RundownEntries,
|
||||||
timedEventOrder: EntryId[],
|
flatOrder: EntryId[],
|
||||||
automationId: string,
|
automationId: string,
|
||||||
): EntryId | undefined {
|
): EntryId | undefined {
|
||||||
for (let i = 0; i < timedEventOrder.length; i++) {
|
for (let i = 0; i < flatOrder.length; i++) {
|
||||||
const eventId = timedEventOrder[i];
|
const eventId = flatOrder[i];
|
||||||
const event = rundown.entries[eventId];
|
const entry = entries[eventId];
|
||||||
if (isOntimeEvent(event) && event.triggers) {
|
|
||||||
for (const trigger of event.triggers) {
|
// only ontime events can contain triggers
|
||||||
|
if (isOntimeEvent(entry) && entry.triggers) {
|
||||||
|
for (const trigger of entry.triggers) {
|
||||||
if (trigger.automationId === automationId) {
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user