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:
Claude
2026-07-08 15:39:48 +00:00
committed by Carlos Valente
parent 25f0dba83a
commit 86b2132cdc
6 changed files with 102 additions and 26 deletions
@@ -120,6 +120,12 @@ export default function OntimeActionForm({
)}
{selectedAction === 'message-secondary' && (
<>
<label>
Text (leave empty for no change)
<Input {...register(`outputs.${index}.text`)} fluid placeholder='eg: Next up: keynote' />
<Panel.Error>{rowErrors?.text?.message}</Panel.Error>
</label>
<label>
Timer secondary source
<Select<SecondarySource | 'null' | null>
@@ -145,6 +151,7 @@ export default function OntimeActionForm({
/>
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
</label>
</>
)}
<div className={style.test}>{children}</div>
@@ -54,6 +54,16 @@ const eventStaticPropertiesNext = [
'{{eventNext.delay}}',
];
const groupStaticPropertiesNow = [
'{{groupNow.id}}',
'{{groupNow.title}}',
'{{groupNow.note}}',
'{{groupNow.colour}}',
'{{groupNow.timeStart}}',
'{{groupNow.timeEnd}}',
'{{groupNow.duration}}',
];
const staticAuxProperties = (index: 1 | 2 | 3) => [
`{{auxtimer${index}.current}}`,
`{{auxtimer${index}.duration}}`,
@@ -75,6 +85,8 @@ export function makeAutoCompleteList(customFields: CustomFields): string[] {
...Object.entries(customFields).map(([key]) => `{{eventNow.custom.${key}}}`),
...eventStaticPropertiesNext,
...Object.entries(customFields).map(([key]) => `{{eventNext.custom.${key}}}`),
...groupStaticPropertiesNow,
...Object.entries(customFields).map(([key]) => `{{groupNow.custom.${key}}}`),
...staticAuxProperties(1),
...staticAuxProperties(2),
...staticAuxProperties(3),
@@ -163,5 +163,51 @@ describe('parseOutput', () => {
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') {
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
if (maybeOntimeAction.secondarySource === null) {
return {
type: 'ontime',
action: 'message-secondary',
secondarySource: null,
text,
};
}
@@ -240,6 +249,7 @@ function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
type: 'ontime',
action: 'message-secondary',
secondarySource: chooseSecondarySource(maybeOntimeAction.secondarySource),
text,
};
}
@@ -66,6 +66,7 @@ export function toOntimeAction(action: OntimeAction) {
timer: {
secondarySource: action.secondarySource,
},
secondary: action.text,
});
break;
}
@@ -111,9 +111,9 @@ export type OntimeAction =
text?: string;
visible?: boolean;
}
// TODO: when setting a secondary source of type secondary we could specify a value to it
| {
type: 'ontime';
action: OntimeMessageSecondary;
secondarySource: SecondarySource;
text?: string;
};