mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
feat(automation): expose group title and allow setting secondary text
Surface {{groupNow.*}} template variables (title, note, colour, times,
custom fields) in the automation template autocomplete so events inside a
group can reference their group. The runtime store already carries
groupNow, so substitution and filters worked already; this makes it
discoverable.
Extend the message-secondary action with an optional text field so an
automation can set the secondary message content, not just its source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZYPZVdZLWowU7DyzGkWyy
This commit is contained in:
+32
-25
@@ -120,31 +120,38 @@ export default function OntimeActionForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedAction === 'message-secondary' && (
|
{selectedAction === 'message-secondary' && (
|
||||||
<label>
|
<>
|
||||||
Timer secondary source
|
<label>
|
||||||
<Select<SecondarySource | 'null' | null>
|
Text (leave empty for no change)
|
||||||
onValueChange={(value) => {
|
<Input {...register(`outputs.${index}.text`)} fluid placeholder='eg: Next up: keynote' />
|
||||||
// null -> no selection
|
<Panel.Error>{rowErrors?.text?.message}</Panel.Error>
|
||||||
if (value === null) return;
|
</label>
|
||||||
// 'null' -> clear the secondary source
|
<label>
|
||||||
if (value === 'null') {
|
Timer secondary source
|
||||||
setValue(`outputs.${index}.secondarySource`, null, { shouldDirty: true });
|
<Select<SecondarySource | 'null' | null>
|
||||||
return;
|
onValueChange={(value) => {
|
||||||
}
|
// null -> no selection
|
||||||
setValue(`outputs.${index}.secondarySource`, value, { shouldDirty: true });
|
if (value === null) return;
|
||||||
}}
|
// 'null' -> clear the secondary source
|
||||||
value={watch(`outputs.${index}.secondarySource`)}
|
if (value === 'null') {
|
||||||
options={[
|
setValue(`outputs.${index}.secondarySource`, null, { shouldDirty: true });
|
||||||
{ value: null, label: 'Select secondary source' },
|
return;
|
||||||
{ value: 'aux1', label: 'Auxiliary timer 1' },
|
}
|
||||||
{ value: 'aux2', label: 'Auxiliary timer 2' },
|
setValue(`outputs.${index}.secondarySource`, value, { shouldDirty: true });
|
||||||
{ value: 'aux3', label: 'Auxiliary timer 3' },
|
}}
|
||||||
{ value: 'secondary', label: 'Secondary' },
|
value={watch(`outputs.${index}.secondarySource`)}
|
||||||
{ value: 'null', label: 'None' }, // allow the user to clear the secondary source
|
options={[
|
||||||
]}
|
{ value: null, label: 'Select secondary source' },
|
||||||
/>
|
{ value: 'aux1', label: 'Auxiliary timer 1' },
|
||||||
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
{ value: 'aux2', label: 'Auxiliary timer 2' },
|
||||||
</label>
|
{ value: 'aux3', label: 'Auxiliary timer 3' },
|
||||||
|
{ value: 'secondary', label: 'Secondary' },
|
||||||
|
{ value: 'null', label: 'None' }, // allow the user to clear the secondary source
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={style.test}>{children}</div>
|
<div className={style.test}>{children}</div>
|
||||||
|
|||||||
+12
@@ -54,6 +54,16 @@ const eventStaticPropertiesNext = [
|
|||||||
'{{eventNext.delay}}',
|
'{{eventNext.delay}}',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const groupStaticPropertiesNow = [
|
||||||
|
'{{groupNow.id}}',
|
||||||
|
'{{groupNow.title}}',
|
||||||
|
'{{groupNow.note}}',
|
||||||
|
'{{groupNow.colour}}',
|
||||||
|
'{{groupNow.timeStart}}',
|
||||||
|
'{{groupNow.timeEnd}}',
|
||||||
|
'{{groupNow.duration}}',
|
||||||
|
];
|
||||||
|
|
||||||
const staticAuxProperties = (index: 1 | 2 | 3) => [
|
const staticAuxProperties = (index: 1 | 2 | 3) => [
|
||||||
`{{auxtimer${index}.current}}`,
|
`{{auxtimer${index}.current}}`,
|
||||||
`{{auxtimer${index}.duration}}`,
|
`{{auxtimer${index}.duration}}`,
|
||||||
@@ -75,6 +85,8 @@ export function makeAutoCompleteList(customFields: CustomFields): string[] {
|
|||||||
...Object.entries(customFields).map(([key]) => `{{eventNow.custom.${key}}}`),
|
...Object.entries(customFields).map(([key]) => `{{eventNow.custom.${key}}}`),
|
||||||
...eventStaticPropertiesNext,
|
...eventStaticPropertiesNext,
|
||||||
...Object.entries(customFields).map(([key]) => `{{eventNext.custom.${key}}}`),
|
...Object.entries(customFields).map(([key]) => `{{eventNext.custom.${key}}}`),
|
||||||
|
...groupStaticPropertiesNow,
|
||||||
|
...Object.entries(customFields).map(([key]) => `{{groupNow.custom.${key}}}`),
|
||||||
...staticAuxProperties(1),
|
...staticAuxProperties(1),
|
||||||
...staticAuxProperties(2),
|
...staticAuxProperties(2),
|
||||||
...staticAuxProperties(3),
|
...staticAuxProperties(3),
|
||||||
|
|||||||
@@ -163,5 +163,51 @@ describe('parseOutput', () => {
|
|||||||
secondarySource: 'secondary',
|
secondarySource: 'secondary',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses message-secondary with a text value', () => {
|
||||||
|
expect(
|
||||||
|
parseOutput({
|
||||||
|
type: 'ontime',
|
||||||
|
action: 'message-secondary',
|
||||||
|
secondarySource: 'secondary',
|
||||||
|
text: 'hello',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
secondarySource: 'secondary',
|
||||||
|
text: 'hello',
|
||||||
|
});
|
||||||
|
// an empty text is treated as no change
|
||||||
|
expect(
|
||||||
|
parseOutput({
|
||||||
|
type: 'ontime',
|
||||||
|
action: 'message-secondary',
|
||||||
|
secondarySource: 'secondary',
|
||||||
|
text: '',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
secondarySource: 'secondary',
|
||||||
|
text: undefined,
|
||||||
|
});
|
||||||
|
// text can be set while clearing the secondary source
|
||||||
|
expect(
|
||||||
|
parseOutput({
|
||||||
|
type: 'ontime',
|
||||||
|
action: 'message-secondary',
|
||||||
|
secondarySource: null,
|
||||||
|
text: 'hello',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
secondarySource: null,
|
||||||
|
text: 'hello',
|
||||||
|
});
|
||||||
|
expect(() =>
|
||||||
|
parseOutput({
|
||||||
|
type: 'ontime',
|
||||||
|
action: 'message-secondary',
|
||||||
|
secondarySource: 'secondary',
|
||||||
|
text: 123,
|
||||||
|
}),
|
||||||
|
).toThrow('Unexpected payload type:');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -226,12 +226,21 @@ function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
|
|||||||
|
|
||||||
if (maybeOntimeAction.action === 'message-secondary') {
|
if (maybeOntimeAction.action === 'message-secondary') {
|
||||||
assert.hasKeys(maybeOntimeAction, ['secondarySource']);
|
assert.hasKeys(maybeOntimeAction, ['secondarySource']);
|
||||||
|
|
||||||
|
// the secondary text is optional, an empty string is treated as no change
|
||||||
|
let text: string | undefined = undefined;
|
||||||
|
if ('text' in maybeOntimeAction) {
|
||||||
|
assert.isString(maybeOntimeAction.text);
|
||||||
|
text = indeterminateText(maybeOntimeAction.text);
|
||||||
|
}
|
||||||
|
|
||||||
// null is used to clear the secondary source
|
// null is used to clear the secondary source
|
||||||
if (maybeOntimeAction.secondarySource === null) {
|
if (maybeOntimeAction.secondarySource === null) {
|
||||||
return {
|
return {
|
||||||
type: 'ontime',
|
type: 'ontime',
|
||||||
action: 'message-secondary',
|
action: 'message-secondary',
|
||||||
secondarySource: null,
|
secondarySource: null,
|
||||||
|
text,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +249,7 @@ function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
|
|||||||
type: 'ontime',
|
type: 'ontime',
|
||||||
action: 'message-secondary',
|
action: 'message-secondary',
|
||||||
secondarySource: chooseSecondarySource(maybeOntimeAction.secondarySource),
|
secondarySource: chooseSecondarySource(maybeOntimeAction.secondarySource),
|
||||||
|
text,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function toOntimeAction(action: OntimeAction) {
|
|||||||
timer: {
|
timer: {
|
||||||
secondarySource: action.secondarySource,
|
secondarySource: action.secondarySource,
|
||||||
},
|
},
|
||||||
|
secondary: action.text,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ export type OntimeAction =
|
|||||||
text?: string;
|
text?: string;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
}
|
}
|
||||||
// TODO: when setting a secondary source of type secondary we could specify a value to it
|
|
||||||
| {
|
| {
|
||||||
type: 'ontime';
|
type: 'ontime';
|
||||||
action: OntimeMessageSecondary;
|
action: OntimeMessageSecondary;
|
||||||
secondarySource: SecondarySource;
|
secondarySource: SecondarySource;
|
||||||
|
text?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user