mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 15:39:11 +00:00
fix: automation form validation
This commit is contained in:
committed by
Carlos Valente
parent
166160dda9
commit
701f24cece
@@ -230,13 +230,18 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
|||||||
<div key={key} className={style.filterSection}>
|
<div key={key} className={style.filterSection}>
|
||||||
<label>
|
<label>
|
||||||
Runtime data source
|
Runtime data source
|
||||||
<Select
|
<Select<string | null>
|
||||||
value={watch(`filters.${index}.field`)}
|
// need to normalize '' to null for the Select to show the placeholder
|
||||||
onValueChange={(value: string | null) => {
|
value={watch(`filters.${index}.field`) || null}
|
||||||
|
onValueChange={(value) => {
|
||||||
if (value === null) return;
|
if (value === null) return;
|
||||||
setValue(`filters.${index}.field`, value, { shouldDirty: true });
|
setValue(`filters.${index}.field`, value, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
options={fieldList.map(({ value, label }) => ({ value, label }))}
|
options={fieldList.map(({ value, label }) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
disabled: value === null,
|
||||||
|
}))}
|
||||||
aria-label='Event field'
|
aria-label='Event field'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
||||||
@@ -288,7 +293,8 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
|||||||
<div className={style.innerColumn}>
|
<div className={style.innerColumn}>
|
||||||
<h3>Outputs</h3>
|
<h3>Outputs</h3>
|
||||||
<Info>
|
<Info>
|
||||||
Automation outputs can be used to send data from Ontime to external software.
|
Automation outputs can be used to send data from Ontime to external software <br />
|
||||||
|
or to change properties of Ontime itself.
|
||||||
<ExternalLink href={integrationsDocsUrl}>See the documentation for templates</ExternalLink>
|
<ExternalLink href={integrationsDocsUrl}>See the documentation for templates</ExternalLink>
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -97,15 +97,14 @@ export default function OntimeActionForm({
|
|||||||
<label>
|
<label>
|
||||||
Visibility
|
Visibility
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value: boolean | 'untouched' | null) => {
|
onValueChange={(value) => {
|
||||||
if (value === null) return;
|
// we need to translate the null to undefined so it becomes 'untouched'
|
||||||
// we need to translate the undefined value to 'untouched'
|
const translatedValue = value === null ? undefined : value;
|
||||||
const translatedValue = value === 'untouched' ? undefined : (value as boolean | undefined);
|
|
||||||
setValue(`outputs.${index}.visible`, translatedValue, { shouldDirty: true });
|
setValue(`outputs.${index}.visible`, translatedValue, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
value={watch(`outputs.${index}.visible`) === undefined ? 'untouched' : watch(`outputs.${index}.visible`)}
|
value={watch(`outputs.${index}.visible`)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'untouched', label: 'Untouched' },
|
{ value: null, label: 'Untouched' },
|
||||||
{ value: true, label: 'Show' },
|
{ value: true, label: 'Show' },
|
||||||
{ value: false, label: 'Hide' },
|
{ value: false, label: 'Hide' },
|
||||||
]}
|
]}
|
||||||
@@ -118,10 +117,16 @@ export default function OntimeActionForm({
|
|||||||
{selectedAction === 'message-secondary' && (
|
{selectedAction === 'message-secondary' && (
|
||||||
<label>
|
<label>
|
||||||
Timer secondary source
|
Timer secondary source
|
||||||
<Select
|
<Select<SecondarySource | 'null' | null>
|
||||||
onValueChange={(value: SecondarySource | null) => {
|
onValueChange={(value) => {
|
||||||
|
// null -> no selection
|
||||||
if (value === null) return;
|
if (value === null) return;
|
||||||
setValue(`outputs.${index}.secondarySource`, value as SecondarySource, { shouldDirty: true });
|
// 'null' -> clear the secondary source
|
||||||
|
if (value === 'null') {
|
||||||
|
setValue(`outputs.${index}.secondarySource`, null, { shouldDirty: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue(`outputs.${index}.secondarySource`, value, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
value={watch(`outputs.${index}.secondarySource`)}
|
value={watch(`outputs.${index}.secondarySource`)}
|
||||||
options={[
|
options={[
|
||||||
@@ -129,13 +134,14 @@ export default function OntimeActionForm({
|
|||||||
{ value: 'aux1', label: 'Auxiliary timer 1' },
|
{ value: 'aux1', label: 'Auxiliary timer 1' },
|
||||||
{ value: 'aux2', label: 'Auxiliary timer 2' },
|
{ value: 'aux2', label: 'Auxiliary timer 2' },
|
||||||
{ value: 'aux3', label: 'Auxiliary timer 3' },
|
{ value: 'aux3', label: 'Auxiliary timer 3' },
|
||||||
{ value: 'external', label: 'External' },
|
{ value: 'secondary', label: 'Secondary' },
|
||||||
{ value: 'null', label: 'None' },
|
{ value: 'null', label: 'None' }, // allow the user to clear the secondary source
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={style.test}>{children}</div>
|
<div className={style.test}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function isAutomation(automation: AutomationDTO | Automation): automation
|
|||||||
}
|
}
|
||||||
|
|
||||||
const staticSelectProperties = [
|
const staticSelectProperties = [
|
||||||
|
{ value: null, label: 'Select field' },
|
||||||
{ value: 'eventNow.id', label: 'ID' },
|
{ value: 'eventNow.id', label: 'ID' },
|
||||||
{ value: 'eventNow.title', label: 'Title' },
|
{ value: 'eventNow.title', label: 'Title' },
|
||||||
{ value: 'eventNow.cue', label: 'Cue' },
|
{ value: 'eventNow.cue', label: 'Cue' },
|
||||||
@@ -42,7 +43,7 @@ const staticNextSelectProperties = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type SelectableField = {
|
type SelectableField = {
|
||||||
value: string; // string encodes path in runtime state object
|
value: string | null; // string encodes path in runtime state object
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -124,8 +124,14 @@ export const validateTestPayload = [
|
|||||||
body('action').if(body('type').equals('ontime')).isString().trim(),
|
body('action').if(body('type').equals('ontime')).isString().trim(),
|
||||||
body('text').if(body('type').equals('ontime')).optional().isString().trim(),
|
body('text').if(body('type').equals('ontime')).optional().isString().trim(),
|
||||||
body('time').if(body('type').equals('ontime')).optional().isString().trim(),
|
body('time').if(body('type').equals('ontime')).optional().isString().trim(),
|
||||||
body('visible').if(body('type').equals('ontime')).optional().isString().trim(),
|
body('visible').if(body('type').equals('ontime')).optional().isBoolean(),
|
||||||
body('secondarySource').if(body('type').equals('ontime')).optional().isString().trim(),
|
// secondary source can be a enum case or null to clear it
|
||||||
|
body('secondarySource')
|
||||||
|
.if(body('type').equals('ontime'))
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.if((value) => value !== null)
|
||||||
|
.isString()
|
||||||
|
.trim(),
|
||||||
|
|
||||||
requestValidationFunction,
|
requestValidationFunction,
|
||||||
];
|
];
|
||||||
@@ -222,8 +228,16 @@ function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
|
|||||||
|
|
||||||
if (maybeOntimeAction.action === 'message-secondary') {
|
if (maybeOntimeAction.action === 'message-secondary') {
|
||||||
assert.hasKeys(maybeOntimeAction, ['secondarySource']);
|
assert.hasKeys(maybeOntimeAction, ['secondarySource']);
|
||||||
assert.isString(maybeOntimeAction.secondarySource);
|
// null is used to clear the secondary source
|
||||||
|
if (maybeOntimeAction.secondarySource === null) {
|
||||||
|
return {
|
||||||
|
type: 'ontime',
|
||||||
|
action: 'message-secondary',
|
||||||
|
secondarySource: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.isString(maybeOntimeAction.secondarySource);
|
||||||
return {
|
return {
|
||||||
type: 'ontime',
|
type: 'ontime',
|
||||||
action: 'message-secondary',
|
action: 'message-secondary',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DeepPartial } from 'ts-essentials';
|
|||||||
|
|
||||||
import { throttle } from '../../utils/throttle.js';
|
import { throttle } from '../../utils/throttle.js';
|
||||||
import type { StoreGetter, PublishFn } from '../../stores/EventStore.js';
|
import type { StoreGetter, PublishFn } from '../../stores/EventStore.js';
|
||||||
|
import { withoutUndefinedValues } from '../../../../../packages/utils/src/common/objectUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a throttled version of the set function
|
* Create a throttled version of the set function
|
||||||
@@ -42,7 +43,10 @@ export function patch(patch: DeepPartial<MessageState>): MessageState {
|
|||||||
// make a copy of the state in store
|
// make a copy of the state in store
|
||||||
const newState = { ...getState() };
|
const newState = { ...getState() };
|
||||||
|
|
||||||
if ('timer' in patch) newState.timer = { ...newState.timer, ...patch.timer };
|
if (patch.timer !== undefined) {
|
||||||
|
const sanitisedTimer = withoutUndefinedValues(patch.timer);
|
||||||
|
newState.timer = { ...newState.timer, ...sanitisedTimer };
|
||||||
|
}
|
||||||
if ('secondary' in patch && patch.secondary !== undefined) newState.secondary = patch.secondary;
|
if ('secondary' in patch && patch.secondary !== undefined) newState.secondary = patch.secondary;
|
||||||
|
|
||||||
throttledSet('message', newState);
|
throttledSet('message', newState);
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ 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;
|
||||||
|
|||||||
@@ -17,6 +17,19 @@ export function getPropertyFromPath<T extends object>(path: string, obj: T): unk
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether an object is empty
|
||||||
|
*/
|
||||||
export function isObjectEmpty(obj: object): boolean {
|
export function isObjectEmpty(obj: object): boolean {
|
||||||
return Object.keys(obj).length === 0;
|
return Object.keys(obj).length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a copy of the object without the properties which have undefined values
|
||||||
|
*/
|
||||||
|
export function withoutUndefinedValues<T extends Record<string, unknown>>(
|
||||||
|
obj: T,
|
||||||
|
): { [K in keyof T]: Exclude<T[K], undefined> } {
|
||||||
|
Object.keys(obj).forEach((key) => obj[key] === undefined && delete obj[key]);
|
||||||
|
return obj as { [K in keyof T]: Exclude<T[K], undefined> };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user