diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldEntry.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldEntry.tsx
index 18a7873926..bbea896aa1 100644
--- a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldEntry.tsx
+++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldEntry.tsx
@@ -4,6 +4,7 @@ import { IoPencil } from '@react-icons/all-files/io5/IoPencil';
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
import { CustomField, CustomFieldLabel } from 'ontime-types';
+import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
import Swatch from '../../../../../common/components/input/colour-input/Swatch';
import CustomFieldForm from './CustomFieldForm';
@@ -36,18 +37,25 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
onSubmit={handleEdit}
initialColour={colour}
initialLabel={label}
+ initialKey={field}
/>
);
}
+ console.log(field)
+
return (
|
{label} |
+
+ {/* TODO: better description */}
+ {field}
+ |
void;
initialColour?: string;
initialLabel?: string;
+ initialKey?: string;
}
export default function CustomFieldForm(props: CustomFieldsFormProps) {
- const { onSubmit, onCancel, initialColour, initialLabel } = props;
+ const { onSubmit, onCancel, initialColour, initialLabel, initialKey } = props;
const { data } = useCustomFields();
// we use this to force an update
@@ -34,7 +35,7 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
getValues,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm({
- defaultValues: { label: initialLabel || '', colour: initialColour || '' },
+ defaultValues: { label: initialLabel || '', colour: initialColour || '', key: initialKey || '' },
resetOptions: {
keepDirtyValues: true,
},
@@ -75,9 +76,10 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
setValue('key', getValues('label').replaceAll(' ', '_')),
validate: (value) => {
if (value.trim().length === 0) return 'Required field';
- if (!isAlphanumeric(value)) return 'Only alphanumeric characters are allowed';
+ if (!isAlphanumericWithSpace(value)) return 'Only alphanumeric characters and space are allowed';
if (Object.keys(data).includes(value)) return 'Custom fields must be unique';
return true;
},
@@ -87,6 +89,11 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
autoComplete='off'
/>
+
+ {/* TODO: better style and description */}
+ Key (The key is generated from the label for use in Integrations and API)
+
+
Colour
diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFields.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFields.tsx
index aabf23dfe2..9c68b81d2c 100644
--- a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFields.tsx
+++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFields.tsx
@@ -73,6 +73,7 @@ export default function CustomFields() {
Colour |
Name |
+ |
|
diff --git a/apps/server/src/api-data/custom-fields/customFields.validation.ts b/apps/server/src/api-data/custom-fields/customFields.validation.ts
index b53bd9a676..2c67503e31 100644
--- a/apps/server/src/api-data/custom-fields/customFields.validation.ts
+++ b/apps/server/src/api-data/custom-fields/customFields.validation.ts
@@ -1,4 +1,4 @@
-import { isAlphanumeric } from 'ontime-utils';
+import { isAlphanumericWithSpace } from 'ontime-utils';
import { Request, Response, NextFunction } from 'express';
import { body, param, validationResult } from 'express-validator';
@@ -9,7 +9,7 @@ export const validateCustomField = [
.isString()
.trim()
.custom((value) => {
- return isAlphanumeric(value);
+ return isAlphanumericWithSpace(value);
}),
body('type').exists().isString().trim(),
body('colour').exists().isString().trim(),
diff --git a/apps/server/src/services/integration-service/OscIntegration.ts b/apps/server/src/services/integration-service/OscIntegration.ts
index 51f50253b0..08f86c535c 100644
--- a/apps/server/src/services/integration-service/OscIntegration.ts
+++ b/apps/server/src/services/integration-service/OscIntegration.ts
@@ -78,6 +78,7 @@ export class OscIntegration implements IIntegration {
describe('createCustomField()', () => {
it('creates a field from given parameters', async () => {
const expected = {
- lighting: {
+ Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
@@ -942,19 +942,19 @@ describe('custom fields', () => {
await createCustomField({ label: 'Sound', type: 'string', colour: 'blue' });
const expected = {
- lighting: {
+ Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
- sound: {
+ Sound: {
label: 'Sound',
type: 'string',
colour: 'green',
},
};
- const customField = await editCustomField('sound', { label: 'Sound', type: 'string', colour: 'green' });
+ const customField = await editCustomField('Sound', { label: 'Sound', type: 'string', colour: 'green' });
expect(customFieldChangelog).toStrictEqual(new Map());
expect(customField).toStrictEqual(expected);
@@ -964,17 +964,17 @@ describe('custom fields', () => {
const created = await createCustomField({ label: 'Video', type: 'string', colour: 'red' });
const expected = {
- lighting: {
+ Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
- sound: {
+ Sound: {
label: 'Sound',
type: 'string',
colour: 'green',
},
- video: {
+ Video: {
label: 'Video',
type: 'string',
colour: 'red',
@@ -984,17 +984,17 @@ describe('custom fields', () => {
expect(created).toStrictEqual(expected);
const expectedAfter = {
- lighting: {
+ Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
- sound: {
+ Sound: {
label: 'Sound',
type: 'string',
colour: 'green',
},
- av: {
+ AV: {
label: 'AV',
type: 'string',
colour: 'red',
@@ -1003,10 +1003,10 @@ describe('custom fields', () => {
// We need to flush all scheduled tasks for the generate function to settle
vi.useFakeTimers();
- const customField = await editCustomField('video', { label: 'AV', type: 'string', colour: 'red' });
+ const customField = await editCustomField('Video', { label: 'AV', type: 'string', colour: 'red' });
expect(customField).toStrictEqual(expectedAfter);
- expect(customFieldChangelog).toStrictEqual(new Map([['video', 'av']]));
- await editCustomField('av', { label: 'video' });
+ expect(customFieldChangelog).toStrictEqual(new Map([['Video', 'AV']]));
+ await editCustomField('AV', { label: 'Video' });
vi.runAllTimers();
expect(customFieldChangelog).toStrictEqual(new Map());
vi.useRealTimers();
@@ -1016,19 +1016,19 @@ describe('custom fields', () => {
describe('removeCustomField()', () => {
it('deletes a field with a given label', async () => {
const expected = {
- lighting: {
+ Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
- video: {
- label: 'video',
+ Video: {
+ label: 'Video',
type: 'string',
colour: 'red',
},
};
- const customField = await removeCustomField('sound');
+ const customField = await removeCustomField('Sound');
expect(customField).toStrictEqual(expected);
});
diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts
index d42591bbd8..448c9c7b20 100644
--- a/apps/server/src/services/rundown-service/rundownCache.ts
+++ b/apps/server/src/services/rundown-service/rundownCache.ts
@@ -11,7 +11,15 @@ import {
OntimeRundownEntry,
PlayableEvent,
} from 'ontime-types';
-import { generateId, insertAtIndex, reorderArray, swapEventData, getTimeFromPrevious, isNewLatest } from 'ontime-utils';
+import {
+ generateId,
+ insertAtIndex,
+ reorderArray,
+ swapEventData,
+ getTimeFromPrevious,
+ isNewLatest,
+ customFieldLabelToKey,
+} from 'ontime-utils';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { createPatch } from '../../utils/parser.js';
import { apply } from './delayUtils.js';
@@ -447,7 +455,7 @@ function scheduleCustomFieldPersist(persistedCustomFields: CustomFields) {
*/
export const createCustomField = async (field: CustomField) => {
const { label, type, colour } = field;
- const key = label.toLowerCase();
+ const key = customFieldLabelToKey(label);
// check if label already exists
const alreadyExists = Object.hasOwn(persistedCustomFields, key);
@@ -479,7 +487,7 @@ export const editCustomField = async (key: string, newField: Partial {
it('enforce name cohesion', () => {
const customFields: CustomFields = {
- test: { label: 'New Name', type: 'string', colour: 'red' },
+ test: { label: 'NewName', type: 'string', colour: 'red' },
};
const expectedCustomFields: CustomFields = {
- 'New Name': { label: 'New Name', type: 'string', colour: 'red' },
+ NewName: { label: 'NewName', type: 'string', colour: 'red' },
+ };
+ const sanitationResult = sanitiseCustomFields(customFields);
+ expect(sanitationResult).toStrictEqual(expectedCustomFields);
+ });
+
+ it('allow old keys', () => {
+ const customFields: CustomFields = {
+ test: { label: 'Test', type: 'string', colour: 'red' },
+ };
+ const expectedCustomFields: CustomFields = {
+ test: { label: 'Test', type: 'string', colour: 'red' },
+ };
+ const sanitationResult = sanitiseCustomFields(customFields);
+ expect(sanitationResult).toStrictEqual(expectedCustomFields);
+ });
+
+ it('labels with space', () => {
+ const customFields: CustomFields = {
+ Test_with_Space: { label: 'Test with Space', type: 'string', colour: 'red' },
+ };
+ const expectedCustomFields: CustomFields = {
+ Test_with_Space: { label: 'Test with Space', type: 'string', colour: 'red' },
};
const sanitationResult = sanitiseCustomFields(customFields);
expect(sanitationResult).toStrictEqual(expectedCustomFields);
diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts
index 469e9605f6..db95bca385 100644
--- a/apps/server/src/utils/parserFunctions.ts
+++ b/apps/server/src/utils/parserFunctions.ts
@@ -19,7 +19,13 @@ import {
isOntimeDelay,
isOntimeEvent,
} from 'ontime-types';
-import { generateId, getErrorMessage, getLastEvent } from 'ontime-utils';
+import {
+ customFieldLabelToKey,
+ generateId,
+ getErrorMessage,
+ getLastEvent,
+ isAlphanumericWithSpace,
+} from 'ontime-utils';
import { dbModel } from '../models/dataModel.js';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
@@ -305,12 +311,18 @@ export function parseCustomFields(data: Partial, emitError?: Erro
export function sanitiseCustomFields(data: object): CustomFields {
const newCustomFields: CustomFields = {};
- for (const [_key, field] of Object.entries(data)) {
+ for (const [originalKey, field] of Object.entries(data)) {
if (!isValidField(field)) {
continue;
}
- const key = field.label;
+ if (!isAlphanumericWithSpace(field.label)) {
+ continue;
+ }
+
+ const keyFromLabel = customFieldLabelToKey(field.label);
+ //Test label and key cohesion, but allow old lowercased keys to stay
+ const key = originalKey.toLocaleLowerCase() === keyFromLabel.toLocaleLowerCase() ? originalKey : keyFromLabel;
if (key in newCustomFields) {
continue;
}
diff --git a/packages/utils/index.ts b/packages/utils/index.ts
index 7a83511d62..1429824eed 100644
--- a/packages/utils/index.ts
+++ b/packages/utils/index.ts
@@ -53,10 +53,12 @@ export {
removeTrailingZero,
} from './src/date-utils/timeFormatting.js';
export { parseUserTime } from './src/date-utils/parseUserTime.js';
-export { isAlphanumeric } from './src/regex-utils/isAlphanumeric.js';
+export { isAlphanumeric, isAlphanumericWithSpace } from './src/regex-utils/isAlphanumeric.js';
export { isColourHex } from './src/regex-utils/isColourHex.js';
export { splitWhitespace } from './src/regex-utils/splitWhitespace.js';
+export { customFieldLabelToKey } from './src/customField-utils/customFieldLabelToKey.js';
+
// helpers from externals
export { deepmerge } from './src/externals/deepmerge.js';
diff --git a/packages/utils/src/customField-utils/customFieldLabelToKey.ts b/packages/utils/src/customField-utils/customFieldLabelToKey.ts
new file mode 100644
index 0000000000..fd56e29dc0
--- /dev/null
+++ b/packages/utils/src/customField-utils/customFieldLabelToKey.ts
@@ -0,0 +1,12 @@
+import { isAlphanumericWithSpace } from '../regex-utils/isAlphanumeric.js';
+
+/**
+ * @description Transforms a Custom field label into a valid key or returns null if not possible
+ * @returns {string | null}
+ */
+export const customFieldLabelToKey = (label: string): string | null => {
+ if (isAlphanumericWithSpace(label)) {
+ return label.trim().replaceAll(' ', '_');
+ }
+ return null;
+};
diff --git a/packages/utils/src/regex-utils/isAlphanumeric.ts b/packages/utils/src/regex-utils/isAlphanumeric.ts
index c4e514faa0..64b375b93b 100644
--- a/packages/utils/src/regex-utils/isAlphanumeric.ts
+++ b/packages/utils/src/regex-utils/isAlphanumeric.ts
@@ -6,3 +6,12 @@ export const isAlphanumeric = (text: string): boolean => {
const regex = /^[a-z0-9]+$/i;
return regex.test(text);
};
+
+/**
+ * @description Validates a alphanumeric string allow space
+ * @returns {boolean}
+ */
+export const isAlphanumericWithSpace = (text: string): boolean => {
+ const regex = /^[a-z0-9_ ]+$/i;
+ return regex.test(text);
+};
|