mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 16:49:41 +00:00
a4e83dd9a0
The automation form had grown a lifecycle picker that created and deleted global triggers behind the user's back. It broke the model the panel is built on — an automation is what to send, a trigger is when — and left the global triggers section describing itself as a place to rename things made elsewhere. Triggers now belong only to the triggers list and the event editor. The automation form edits a title, filters and outputs, and saves in one request: the two-request save, its partial-failure recovery and the snapshot it reconciled against are all gone with it. An automation with no global trigger offers to make one from its own row, opening the trigger form with the automation preselected, so the connection is still one click away without the form pretending to own it. Other changes in the same pass: - the form uses the compact modal instead of the wide one. Nothing in it justified 1800px, and output fields now pair up two to a row rather than stretching across four columns - a blank automation gets its own header button beside Start from recipe, instead of being a footnote under the recipe list - the settings form seeds itself from the query when it resolves, as the other settings panels do. It was showing automations as OFF while they were on - the filter operator list goes back to the three master offered, which makes the note about not_contains unnecessary rather than explanatory - comments that narrated the change rather than explaining the code Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
204 lines
5.7 KiB
TypeScript
204 lines
5.7 KiB
TypeScript
import type {
|
|
Automation,
|
|
AutomationDTO,
|
|
AutomationSettings,
|
|
NormalisedAutomation,
|
|
ProjectRundowns,
|
|
Trigger,
|
|
TriggerDTO,
|
|
} from 'ontime-types';
|
|
import { deleteAtIndex, generateId } from 'ontime-utils';
|
|
|
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
|
import { isAutomationUsed } from './automation.utils.js';
|
|
|
|
/**
|
|
* Gets a copy of the stored automation settings
|
|
*/
|
|
export function getAutomationSettings(): AutomationSettings {
|
|
return getDataProvider().getAutomation();
|
|
}
|
|
|
|
/**
|
|
* Gets the enabled status of the automations
|
|
*/
|
|
export function getAutomationsEnabled(): boolean {
|
|
return getAutomationSettings().enabledAutomations;
|
|
}
|
|
|
|
/**
|
|
* Gets a copy of the stored automation triggers
|
|
*/
|
|
export function getAutomationTriggers(): Trigger[] {
|
|
return getAutomationSettings().triggers;
|
|
}
|
|
|
|
/**
|
|
* Gets a copy of the stored automations
|
|
*/
|
|
export function getAutomations(): NormalisedAutomation {
|
|
return getAutomationSettings().automations;
|
|
}
|
|
|
|
/**
|
|
* Patches the automation settings object
|
|
*/
|
|
export async function editAutomationSettings(settings: Partial<AutomationSettings>): Promise<AutomationSettings> {
|
|
await saveChanges(settings);
|
|
return getAutomationSettings();
|
|
}
|
|
|
|
/**
|
|
* Adds a validated automation to the store
|
|
*/
|
|
export async function addTrigger(newTrigger: TriggerDTO): Promise<Trigger> {
|
|
const triggers = getAutomationTriggers();
|
|
const id = getUniqueTriggerId(triggers);
|
|
const trigger = { ...newTrigger, id };
|
|
triggers.push(trigger);
|
|
await saveChanges({ triggers });
|
|
return trigger;
|
|
}
|
|
|
|
/**
|
|
* Patches an existing automation trigger
|
|
*/
|
|
export async function editTrigger(id: string, newTrigger: TriggerDTO): Promise<Trigger> {
|
|
const triggers = getAutomationTriggers();
|
|
const index = triggers.findIndex((trigger) => trigger.id === id);
|
|
|
|
if (index === -1) {
|
|
throw new Error(`Automation with id ${id} not found`);
|
|
}
|
|
|
|
triggers[index] = { ...triggers[index], ...newTrigger, id };
|
|
await saveChanges({ triggers });
|
|
const updatedTrigger = triggers[index];
|
|
if (!updatedTrigger) {
|
|
throw new Error(`Failed to update trigger with id ${id}`);
|
|
}
|
|
return updatedTrigger;
|
|
}
|
|
|
|
/**
|
|
* Deletes an automation trigger given its ID
|
|
*/
|
|
export async function deleteTrigger(id: string): Promise<void> {
|
|
const triggers = getAutomationTriggers();
|
|
const index = triggers.findIndex((trigger) => trigger.id === id);
|
|
|
|
// deleting is idempotent, as it is in deleteAutomation: the state the caller asked for
|
|
// already holds, and erroring would only punish a client that raced another one
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
|
|
await saveChanges({ triggers: deleteAtIndex(index, triggers) });
|
|
}
|
|
|
|
/**
|
|
* Deletes all project automation triggers
|
|
*/
|
|
export async function deleteAllTriggers(): Promise<void> {
|
|
await saveChanges({ triggers: [] });
|
|
}
|
|
|
|
/**
|
|
* Deletes all project automation triggers and automations
|
|
* We do this together to avoid issues with missing references
|
|
*/
|
|
export async function deleteAll() {
|
|
await saveChanges({ triggers: [], automations: {} });
|
|
}
|
|
|
|
/**
|
|
* Adds a validated automation to the store
|
|
*/
|
|
export async function addAutomation(newAutomation: AutomationDTO): Promise<Automation> {
|
|
const automations = getAutomations();
|
|
const id = getUniqueAutomationId(automations);
|
|
automations[id] = { ...newAutomation, id };
|
|
await saveChanges({ automations });
|
|
return automations[id];
|
|
}
|
|
|
|
/**
|
|
* Updates an existing automation with a new entry
|
|
*/
|
|
export async function editAutomation(id: string, newAutomation: AutomationDTO): Promise<Automation> {
|
|
const automations = getAutomations();
|
|
if (!Object.hasOwn(automations, id)) {
|
|
throw new Error(`Automation with id ${id} not found`);
|
|
}
|
|
|
|
automations[id] = { ...newAutomation, id };
|
|
await saveChanges({ automations });
|
|
return automations[id];
|
|
}
|
|
|
|
/**
|
|
* Deletes a automation given its ID
|
|
*/
|
|
export async function deleteAutomation(projectRundowns: ProjectRundowns, automationId: string): Promise<void> {
|
|
const automations = getAutomations();
|
|
// ignore request if automation does not exist
|
|
if (!Object.hasOwn(automations, automationId)) {
|
|
return;
|
|
}
|
|
|
|
// prevent deleting an automation that is in use in events, the user has to unlink it there
|
|
const isInUse = isAutomationUsed(projectRundowns, automationId);
|
|
if (isInUse) {
|
|
throw new Error(`Unable to delete automation used in rundown: ${isInUse[0]}, in event with ID: ${isInUse[1]}`);
|
|
}
|
|
|
|
// a global trigger without its automation is dead data, so it goes with it.
|
|
// Both are written in a single patch, there is no state where one outlived the other
|
|
const triggers = getAutomationTriggers().filter((trigger) => trigger.automationId !== automationId);
|
|
|
|
delete automations[automationId];
|
|
await saveChanges({ automations, triggers });
|
|
}
|
|
|
|
/**
|
|
* Internal utility to patch the automation settings
|
|
*/
|
|
async function saveChanges(patch: Partial<AutomationSettings>) {
|
|
const automation = getDataProvider().getAutomation();
|
|
|
|
// remove undefined keys from object, we probably want a better solution
|
|
Object.keys(patch).forEach((key) => {
|
|
const typedKey = key as keyof AutomationSettings;
|
|
if (patch[typedKey] === undefined) {
|
|
delete patch[typedKey];
|
|
}
|
|
});
|
|
await getDataProvider().setAutomation({ ...automation, ...patch });
|
|
}
|
|
|
|
/**
|
|
* Returns an ID guaranteed to be unique in an array
|
|
*/
|
|
function getUniqueTriggerId(triggers: Trigger[]): string {
|
|
let id = '';
|
|
do {
|
|
id = generateId();
|
|
} while (isInArray(id));
|
|
|
|
function isInArray(id: string): boolean {
|
|
return triggers.some((trigger) => trigger.id === id);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Returns an ID guaranteed to be unique in an objects keys
|
|
*/
|
|
function getUniqueAutomationId(automations: NormalisedAutomation): string {
|
|
let id = '';
|
|
do {
|
|
id = generateId();
|
|
} while (Object.hasOwn(automations, id));
|
|
return id;
|
|
}
|