mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-07 15:29:10 +00:00
feat(automation): one step creation, recipes, and a panel that says what it does
Making an automation took two visits to two lists. You wrote the automation in one, then remembered that an automation alone never runs, went to the trigger list, made a trigger, and pointed it back at what you had just made. A first time user who stopped after the first step got a thing that looked finished and did nothing. Lifecycles are now picked on the automation form, as chips. Saving reconciles the triggers behind it, diffed against a snapshot taken when the form opened so a save never removes a trigger the user could not see. Every lifecycle is in one place, including the two that fire continuously, which say so before you pick them rather than after your log fills up. That leaves the trigger list as what it actually is: a place to rename a generated trigger, or to point several differently named ones at the same automation. It says so, and it names the triggers whose automation is gone. New opens one list of starting points: an empty automation, then a handful of recipes for the software people actually pair Ontime with. A recipe is nothing but a pre-filled form, so choosing one opens the ordinary automation form with its values in place. The user reads what it will do, points it at their own gear and saves. Nothing is written until they do, and there is no second creation path to keep working: one request, the same as any other automation. Every recipe targets loopback, so one saved without thinking cannot put traffic on a venue network. The list now answers what an automation does without opening it: when it runs, whether it filters, and what it sends. An automation with no trigger or no output is the common half-finished state and is called out rather than shown as a blank cell. Outputs are cards with their type, a summary and a Test button in the header, which is also where the test says whether it worked: the panel used to send and tell the user nothing either way. Deleting confirms first and names the triggers that go with it, instead of dumping the server's refusal into a stray row under the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { IoChevronForward } from 'react-icons/io5';
|
||||
|
||||
import Modal from '../../../../common/components/modal/Modal';
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
import { getLifecycleLabel } from '../../../../common/constants/timerLifecycle';
|
||||
import { summariseOutputs } from '../../../../common/utils/automationOutputs';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import { automationRecipes, needsTarget, type AutomationRecipe } from './automationRecipes';
|
||||
|
||||
import style from './NewAutomationDialog.module.scss';
|
||||
|
||||
interface NewAutomationDialogProps {
|
||||
onClose: () => void;
|
||||
/** called with the recipe to pre-fill the form with, or null to start from an empty one */
|
||||
onSelect: (recipe: AutomationRecipe | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The single entry point for making an automation: a list of starting points.
|
||||
* Picking one opens the ordinary automation form pre-filled, so a recipe is a head start
|
||||
* rather than a separate kind of object. Nothing is saved until the user saves the form.
|
||||
*/
|
||||
export default function NewAutomationDialog({ onClose, onSelect }: NewAutomationDialogProps) {
|
||||
// OSC is not available in the cloud service, offering those recipes there would be a lie
|
||||
const recipes = isOntimeCloud
|
||||
? automationRecipes.filter((recipe) => !recipe.automation.outputs.some((output) => output.type === 'osc'))
|
||||
: automationRecipes;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
showBackdrop
|
||||
showCloseButton
|
||||
title='New automation'
|
||||
bodyElements={
|
||||
<div className={style.list}>
|
||||
<button type='button' className={style.option} onClick={() => onSelect(null)}>
|
||||
<div className={style.optionText}>
|
||||
<div className={style.optionTitle}>Empty automation</div>
|
||||
<div className={style.optionDescription}>Start from scratch.</div>
|
||||
</div>
|
||||
<IoChevronForward className={style.chevron} />
|
||||
</button>
|
||||
|
||||
<div className={style.listLabel}>
|
||||
Or start from a recipe. Each one opens as a normal automation you can edit before saving.
|
||||
</div>
|
||||
|
||||
{recipes.map((recipe) => (
|
||||
<button
|
||||
type='button'
|
||||
key={recipe.id}
|
||||
className={style.option}
|
||||
onClick={() => onSelect(recipe)}
|
||||
aria-label={`Start from ${recipe.automation.title}`}
|
||||
>
|
||||
<div className={style.optionText}>
|
||||
<div className={style.optionTitle}>{recipe.automation.title}</div>
|
||||
<div className={style.optionDescription}>{recipe.description}</div>
|
||||
<Panel.InlineElements relation='inner' wrap='wrap'>
|
||||
{recipe.triggers.map((cycle) => (
|
||||
<Tag key={cycle}>{getLifecycleLabel(cycle)}</Tag>
|
||||
))}
|
||||
{summariseOutputs(recipe.automation.outputs).map(({ type, label, count }) => (
|
||||
<Tag key={type}>{count > 1 ? `${label} ×${count}` : label}</Tag>
|
||||
))}
|
||||
{needsTarget(recipe) && <Tag variant='warning'>Point it at your device</Tag>}
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
<IoChevronForward className={style.chevron} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user