mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-12 17:49:37 +00:00
feat(automation): streamline automation management
Surface lifecycle and output summaries, guided creation, and clear deletion safeguards in automation settings.
This commit is contained in:
@@ -30,16 +30,16 @@ export default function AutomationPanel({ location }: PanelBaseProps) {
|
||||
/>
|
||||
</div>
|
||||
<div ref={automationsRef}>
|
||||
<AutomationsList automations={data.automations} enabledAutomations={automationState} isLoading={isLoading} />
|
||||
</div>
|
||||
<div ref={triggersRef}>
|
||||
<TriggersList
|
||||
triggers={data.triggers}
|
||||
<AutomationsList
|
||||
automations={data.automations}
|
||||
triggers={data.triggers}
|
||||
enabledAutomations={automationState}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div ref={triggersRef}>
|
||||
<TriggersList triggers={data.triggers} automations={data.automations} isLoading={isLoading} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -94,7 +94,8 @@ export default function AutomationSettingsForm({
|
||||
<Panel.Section>
|
||||
<Info>
|
||||
<span>Control Ontime and share its data with external systems in your workflow.</span>
|
||||
<span>- Automations allow Ontime to send its data on lifecycle triggers.</span>
|
||||
<span>- An automation is what to send: OSC and HTTP messages, or an action inside Ontime.</span>
|
||||
<span>- A trigger is when to send it. Triggers for a single event live in the event editor.</span>
|
||||
<span>- OSC Input tells Ontime to listen to messages on the specific port.</span>
|
||||
<ExternalLink href={oscApiDocsUrl}>See the docs</ExternalLink>
|
||||
</Info>
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: $muted-gray;
|
||||
}
|
||||
+142
-58
@@ -1,18 +1,25 @@
|
||||
import { AutomationDTO, NormalisedAutomation } from 'ontime-types';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Automation, AutomationDTO, NormalisedAutomation, Trigger } from 'ontime-types';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { IoAdd, IoPencil, IoTrash } from 'react-icons/io5';
|
||||
|
||||
import { deleteAutomation } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
import { getLifecycleLabel } from '../../../../common/constants/timerLifecycle';
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import { summariseOutputs } from '../../../../common/utils/automationOutputs';
|
||||
import { cx } from '../../../../common/utils/styleUtils';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import useAppSettingsNavigation from '../../useAppSettingsNavigation';
|
||||
import AutomationForm from './AutomationForm';
|
||||
import { groupTriggersByAutomation, isAutomation } from './automationUtils';
|
||||
import DeleteAutomationDialog from './DeleteAutomationDialog';
|
||||
import NewAutomationDialog from './NewAutomationDialog';
|
||||
|
||||
const automationPlaceholder: AutomationDTO = {
|
||||
import style from './AutomationsList.module.scss';
|
||||
|
||||
const emptyAutomation: AutomationDTO = {
|
||||
title: '',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
@@ -21,37 +28,93 @@ const automationPlaceholder: AutomationDTO = {
|
||||
|
||||
interface AutomationsListProps {
|
||||
automations: NormalisedAutomation;
|
||||
triggers: Trigger[];
|
||||
enabledAutomations?: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default function AutomationsList({ automations, enabledAutomations, isLoading }: AutomationsListProps) {
|
||||
export default function AutomationsList({
|
||||
automations,
|
||||
triggers,
|
||||
enabledAutomations,
|
||||
isLoading,
|
||||
}: AutomationsListProps) {
|
||||
const { refetch } = useAutomationSettings();
|
||||
const [automationFormData, setAutomationFormData] = useState<AutomationDTO | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const { setLocation } = useAppSettingsNavigation();
|
||||
const [editing, setEditing] = useState<Automation | AutomationDTO | null>(null);
|
||||
const [isPickingStart, setIsPickingStart] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Automation | null>(null);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
setDeleteError(null);
|
||||
await deleteAutomation(id);
|
||||
} catch (error) {
|
||||
setDeleteError(maybeAxiosError(error));
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
const lifecyclesByAutomation = useMemo(() => groupTriggersByAutomation(triggers), [triggers]);
|
||||
const automationList = Object.values(automations);
|
||||
|
||||
/** a recipe creates the automation itself, so it lands in the list rather than in a form */
|
||||
const handleCreated = async () => {
|
||||
setIsPickingStart(false);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
const arrayAutomations = Object.keys(automations);
|
||||
const handleStartEmpty = () => {
|
||||
setIsPickingStart(false);
|
||||
setDeleteTarget(null);
|
||||
setEditing(emptyAutomation);
|
||||
};
|
||||
|
||||
const handleDeleted = async () => {
|
||||
setDeleteTarget(null);
|
||||
setEditing(null);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
const startPicking = () => {
|
||||
setEditing(null);
|
||||
setDeleteTarget(null);
|
||||
setIsPickingStart(true);
|
||||
};
|
||||
|
||||
const startEditing = (automation: Automation) => {
|
||||
setIsPickingStart(false);
|
||||
setDeleteTarget(null);
|
||||
setEditing(automation);
|
||||
};
|
||||
|
||||
const startDeleting = (automation: Automation) => {
|
||||
setIsPickingStart(false);
|
||||
setEditing(null);
|
||||
setDeleteTarget(automation);
|
||||
};
|
||||
|
||||
return (
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
{automationFormData !== null && (
|
||||
<AutomationForm automation={automationFormData} onClose={() => setAutomationFormData(null)} />
|
||||
{editing !== null && (
|
||||
<AutomationForm
|
||||
// the form snapshots the automation's lifecycles on mount, so it must never be
|
||||
// reused across two different automations
|
||||
key={isAutomation(editing) ? editing.id : 'new'}
|
||||
automation={editing}
|
||||
triggers={triggers}
|
||||
onClose={() => setEditing(null)}
|
||||
/>
|
||||
)}
|
||||
{isPickingStart && (
|
||||
<NewAutomationDialog
|
||||
onClose={() => setIsPickingStart(false)}
|
||||
onStartEmpty={handleStartEmpty}
|
||||
onCreated={handleCreated}
|
||||
/>
|
||||
)}
|
||||
{deleteTarget !== null && (
|
||||
<DeleteAutomationDialog
|
||||
automation={deleteTarget}
|
||||
attachedTriggers={triggers.filter((trigger) => trigger.automationId === deleteTarget.id)}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onDeleted={handleDeleted}
|
||||
/>
|
||||
)}
|
||||
<Panel.SubHeader>
|
||||
Manage automations
|
||||
<Button onClick={() => setAutomationFormData(automationPlaceholder)}>
|
||||
<Button onClick={startPicking}>
|
||||
New <IoAdd />
|
||||
</Button>
|
||||
</Panel.SubHeader>
|
||||
@@ -60,74 +123,95 @@ export default function AutomationsList({ automations, enabledAutomations, isLoa
|
||||
|
||||
<Panel.Section>
|
||||
{enabledAutomations === false && (
|
||||
<Info>
|
||||
Automations are disabled. You can still manage automation definitions here, but they will not run until
|
||||
enabled.
|
||||
<Info type='warning'>
|
||||
<Info.Body>Automations are off, so nothing in this list will run.</Info.Body>
|
||||
<Info.Footer>
|
||||
<Button size='small' onClick={() => setLocation('automation__settings')}>
|
||||
Go to automation settings
|
||||
</Button>
|
||||
</Info.Footer>
|
||||
</Info>
|
||||
)}
|
||||
|
||||
<Panel.Table>
|
||||
<Panel.Table className={style.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '45%' }}>Title</th>
|
||||
<th style={{ width: '15%' }}>Trigger rule</th>
|
||||
<th style={{ width: '15%' }}>Filters</th>
|
||||
<th style={{ width: '15%' }}>Outputs</th>
|
||||
<th style={{ width: '35%' }}>Title</th>
|
||||
<th style={{ width: '25%' }}>Runs on</th>
|
||||
<th style={{ width: '15%' }}>Filter rule</th>
|
||||
<th style={{ width: '15%' }}>Sends</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!isLoading && arrayAutomations.length === 0 && (
|
||||
{!isLoading && automationList.length === 0 && (
|
||||
<Panel.TableEmpty
|
||||
title='No automations yet'
|
||||
description='An automation sends OSC or HTTP messages, or runs an Ontime action, whenever a trigger fires.'
|
||||
description='An automation sends OSC or HTTP messages, or runs an Ontime action, whenever a trigger fires. Start from a recipe to see one working.'
|
||||
action={
|
||||
<Button variant='primary' onClick={() => setAutomationFormData(automationPlaceholder)}>
|
||||
Create automation <IoAdd />
|
||||
<Button variant='primary' onClick={startPicking}>
|
||||
New automation <IoAdd />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{arrayAutomations.map((automationId) => {
|
||||
if (!Object.hasOwn(automations, automationId)) {
|
||||
return null;
|
||||
}
|
||||
{automationList.map((automation) => {
|
||||
const lifecycles = lifecyclesByAutomation[automation.id] ?? [];
|
||||
const outputs = summariseOutputs(automation.outputs);
|
||||
|
||||
return (
|
||||
<Fragment key={automationId}>
|
||||
<tr>
|
||||
<td>{automations[automationId].title}</td>
|
||||
<td>
|
||||
<Tag>{automations[automationId].filterRule}</Tag>
|
||||
</td>
|
||||
<td>{automations[automationId].filters.length}</td>
|
||||
<td>{automations[automationId].outputs.length}</td>
|
||||
<Panel.InlineElements align='end' relation='inner' as='td'>
|
||||
<tr key={automation.id}>
|
||||
<td>{automation.title}</td>
|
||||
<td>
|
||||
{lifecycles.length === 0 ? (
|
||||
<span className={style.muted}>—</span>
|
||||
) : (
|
||||
<div className={style.tags}>
|
||||
{lifecycles.map((cycle) => (
|
||||
<Tag key={cycle}>{getLifecycleLabel(cycle)}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{automation.filters.length === 0 ? (
|
||||
<span className={style.muted}>—</span>
|
||||
) : (
|
||||
<Tag>{automation.filterRule === 'all' ? 'All filters' : 'Any filter'}</Tag>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className={style.tags}>
|
||||
{outputs.length === 0 ? (
|
||||
<Tag variant='warning'>No outputs</Tag>
|
||||
) : (
|
||||
outputs.map(({ type, label, count }) => (
|
||||
<Tag key={type}>{count > 1 ? `${label} ×${count}` : label}</Tag>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={cx([style.tags, style.actions])}>
|
||||
<IconButton
|
||||
variant='ghosted-white'
|
||||
aria-label='Edit entry'
|
||||
onClick={() => setAutomationFormData(automations[automationId])}
|
||||
onClick={() => startEditing(automation)}
|
||||
>
|
||||
<IoPencil />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant='ghosted-destructive'
|
||||
aria-label='Delete entry'
|
||||
onClick={() => handleDelete(automationId)}
|
||||
onClick={() => startDeleting(automation)}
|
||||
>
|
||||
<IoTrash />
|
||||
</IconButton>
|
||||
</Panel.InlineElements>
|
||||
</tr>
|
||||
</Fragment>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{deleteError && (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
<Panel.Error>{deleteError}</Panel.Error>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
</Panel.Section>
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import axios from 'axios';
|
||||
import type { Automation, Trigger } from 'ontime-types';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { deleteAutomation } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import Dialog from '../../../../common/components/dialog/Dialog';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import { getLifecycleLabel } from '../../../../common/constants/timerLifecycle';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
interface DeleteAutomationDialogProps {
|
||||
automation: Automation;
|
||||
/** global triggers pointing at this automation, they are deleted along with it */
|
||||
attachedTriggers: Trigger[];
|
||||
onCancel: () => void;
|
||||
onDeleted: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting takes the automation's global triggers with it, so say so before it happens rather
|
||||
* than leaving the user to discover it in the triggers list.
|
||||
*
|
||||
* An automation attached to an event is still refused by the server: that reference lives in
|
||||
* the rundown and removing it is an edit to the show, not to this panel.
|
||||
*/
|
||||
export default function DeleteAutomationDialog({
|
||||
automation,
|
||||
attachedTriggers,
|
||||
onCancel,
|
||||
onDeleted,
|
||||
}: DeleteAutomationDialogProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isAttachedToEvent, setIsAttachedToEvent] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setError(null);
|
||||
setIsAttachedToEvent(false);
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteAutomation(automation.id);
|
||||
onDeleted();
|
||||
} catch (error) {
|
||||
setError(maybeAxiosError(error));
|
||||
setIsAttachedToEvent(axios.isAxiosError(error) && error.response?.status === 409);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen
|
||||
onClose={onCancel}
|
||||
showBackdrop
|
||||
showCloseButton
|
||||
title='Delete automation'
|
||||
bodyElements={
|
||||
<Panel.Section>
|
||||
<Panel.Paragraph>
|
||||
Delete <strong>{automation.title}</strong>? This cannot be undone.
|
||||
</Panel.Paragraph>
|
||||
|
||||
{attachedTriggers.length > 0 && (
|
||||
<Info type='warning'>
|
||||
<Info.Title>
|
||||
{attachedTriggers.length === 1
|
||||
? 'Its trigger is deleted with it'
|
||||
: `Its ${attachedTriggers.length} triggers are deleted with it`}
|
||||
</Info.Title>
|
||||
<Info.Body>{attachedTriggers.map((trigger) => getLifecycleLabel(trigger.trigger)).join(', ')}</Info.Body>
|
||||
</Info>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Info type='error'>
|
||||
<Info.Title>Could not delete this automation</Info.Title>
|
||||
<Info.Body>{error}</Info.Body>
|
||||
<Info.Footer>
|
||||
{isAttachedToEvent
|
||||
? 'An automation attached to a single event has to be removed from that event first, in the event editor.'
|
||||
: 'Try again. If the problem persists, check the network log for details.'}
|
||||
</Info.Footer>
|
||||
</Info>
|
||||
)}
|
||||
</Panel.Section>
|
||||
}
|
||||
footerElements={
|
||||
<>
|
||||
<Button onClick={onCancel} disabled={isDeleting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='destructive' onClick={handleDelete} loading={isDeleting}>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NormalisedAutomation, Trigger } from 'ontime-types';
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
|
||||
import { deleteTrigger } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import useAppSettingsNavigation from '../../useAppSettingsNavigation';
|
||||
import { checkDuplicates } from './automationUtils';
|
||||
import TriggerForm from './TriggerForm';
|
||||
import TriggersListItem from './TriggersListItem';
|
||||
@@ -20,13 +20,13 @@ type FormState = {
|
||||
interface TriggersListProps {
|
||||
triggers: Trigger[];
|
||||
automations: NormalisedAutomation;
|
||||
enabledAutomations?: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default function TriggersList({ triggers, automations, enabledAutomations, isLoading }: TriggersListProps) {
|
||||
export default function TriggersList({ triggers, automations, isLoading }: TriggersListProps) {
|
||||
const [formState, setFormState] = useState<FormState>({ isOpen: false, trigger: undefined });
|
||||
const { refetch } = useAutomationSettings();
|
||||
const { setLocation } = useAppSettingsNavigation();
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
|
||||
const openNewForm = () => setFormState({ isOpen: true });
|
||||
@@ -50,6 +50,10 @@ export default function TriggersList({ triggers, automations, enabledAutomations
|
||||
};
|
||||
|
||||
const duplicates = useMemo(() => checkDuplicates(triggers), [triggers]);
|
||||
const orphans = useMemo(
|
||||
() => triggers.filter((trigger) => !Object.hasOwn(automations, trigger.automationId)).length,
|
||||
[triggers, automations],
|
||||
);
|
||||
|
||||
// there is no point letting user creating a trigger if there are no automations
|
||||
const canAdd = Object.keys(automations).length > 0;
|
||||
@@ -66,22 +70,28 @@ export default function TriggersList({ triggers, automations, enabledAutomations
|
||||
/>
|
||||
)}
|
||||
<Panel.SubHeader>
|
||||
Manage triggers
|
||||
Global triggers
|
||||
<Button disabled={!canAdd} onClick={openNewForm}>
|
||||
New <IoAdd />
|
||||
</Button>
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Section>
|
||||
{enabledAutomations === false && (
|
||||
<Info>
|
||||
Automations are disabled. You can still manage triggers here, but they will not run until enabled.
|
||||
</Info>
|
||||
)}
|
||||
<Panel.Description>
|
||||
Triggers are managed from the automation itself. This list is for naming them, or for pointing several
|
||||
differently named triggers at the same automation.
|
||||
</Panel.Description>
|
||||
{duplicates && (
|
||||
<Panel.Error>
|
||||
You have created multiple links between the same trigger and automation which can cause performance
|
||||
issues.
|
||||
You have created multiple links between the same trigger and automation. Duplicate combinations will only
|
||||
fire once per lifecycle event.
|
||||
</Panel.Error>
|
||||
)}
|
||||
{orphans > 0 && (
|
||||
<Panel.Error>
|
||||
{orphans === 1
|
||||
? '1 trigger points at an automation that no longer exists and will never run.'
|
||||
: `${orphans} triggers point at automations that no longer exist and will never run.`}
|
||||
</Panel.Error>
|
||||
)}
|
||||
<Panel.Table>
|
||||
@@ -99,31 +109,32 @@ export default function TriggersList({ triggers, automations, enabledAutomations
|
||||
title='No triggers yet'
|
||||
description={
|
||||
canAdd
|
||||
? 'Triggers run an automation at a given point of the timer lifecycle, like when an event starts or finishes.'
|
||||
: 'Create an automation first, then add a trigger to decide when it should run.'
|
||||
? 'Triggers run an automation at a given point of the timer lifecycle. The usual way to create one is to pick the lifecycles in the automation itself.'
|
||||
: 'Create an automation first, then pick the lifecycles it should run on.'
|
||||
}
|
||||
action={
|
||||
canAdd && (
|
||||
canAdd ? (
|
||||
<Button variant='primary' onClick={openNewForm}>
|
||||
Create trigger <IoAdd />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant='primary' onClick={() => setLocation('automation__automations')}>
|
||||
Go to automations
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{triggers.map((trigger, index) => {
|
||||
return (
|
||||
<Fragment key={trigger.id}>
|
||||
<TriggersListItem
|
||||
automations={automations}
|
||||
trigger={trigger}
|
||||
duplicate={duplicates?.includes(index)}
|
||||
handleEdit={() => openEditForm(trigger)}
|
||||
handleDelete={() => handleDelete(trigger.id)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{triggers.map((trigger, index) => (
|
||||
<TriggersListItem
|
||||
key={trigger.id}
|
||||
automations={automations}
|
||||
trigger={trigger}
|
||||
duplicate={duplicates?.includes(index)}
|
||||
handleEdit={() => openEditForm(trigger)}
|
||||
handleDelete={() => handleDelete(trigger.id)}
|
||||
/>
|
||||
))}
|
||||
{deleteError && (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
|
||||
@@ -16,6 +16,7 @@ interface TriggersListItemProps {
|
||||
|
||||
export default function TriggersListItem(props: TriggersListItemProps) {
|
||||
const { automations, trigger, duplicate, handleEdit, handleDelete } = props;
|
||||
const automation = automations[trigger.automationId];
|
||||
|
||||
return (
|
||||
<tr data-warn={duplicate}>
|
||||
@@ -31,7 +32,8 @@ export default function TriggersListItem(props: TriggersListItemProps) {
|
||||
<Tag>{cycles.find((cycle) => cycle.value === trigger.trigger)?.label}</Tag>
|
||||
</td>
|
||||
<td>
|
||||
<Tag>{automations?.[trigger.automationId]?.title}</Tag>
|
||||
{/* a trigger can outlive the automation it points at, say after a partial project import */}
|
||||
{automation ? <Tag>{automation.title}</Tag> : <Tag variant='warning'>Missing automation</Tag>}
|
||||
</td>
|
||||
<Panel.InlineElements align='end' relation='inner' as='td'>
|
||||
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={handleEdit}>
|
||||
|
||||
@@ -84,11 +84,25 @@ const staticOptions = [
|
||||
{
|
||||
id: 'automation__automations',
|
||||
label: 'Manage automations',
|
||||
keywords: ['osc', 'http', 'webhook', 'integration', 'api', 'output', 'action'],
|
||||
keywords: [
|
||||
'osc',
|
||||
'http',
|
||||
'webhook',
|
||||
'integration',
|
||||
'api',
|
||||
'output',
|
||||
'action',
|
||||
'recipe',
|
||||
'example',
|
||||
'preset',
|
||||
'qlab',
|
||||
'vmix',
|
||||
'companion',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'automation__triggers',
|
||||
label: 'Manage triggers',
|
||||
label: 'Global triggers',
|
||||
keywords: ['lifecycle', 'on load', 'on start', 'on finish', 'on update'],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user