From a4581d8397d8c34741440ca042ae619d7650b4dd Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 12 Oct 2024 11:39:23 +0200 Subject: [PATCH 1/8] also keep upper case when editing --- apps/server/src/services/rundown-service/rundownCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index d42591bbd8..96221435fd 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -479,7 +479,7 @@ export const editCustomField = async (key: string, newField: Partial Date: Sat, 12 Oct 2024 11:59:29 +0200 Subject: [PATCH 2/8] show key in UI --- .../custom-fields/CustomFieldEntry.tsx | 5 +++++ .../custom-fields/CustomFieldForm.tsx | 8 +++++++- .../feature-settings-panel/custom-fields/CustomFields.tsx | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) 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..40e9f2e1be 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'; @@ -48,6 +49,10 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) { {label} + + {/* TODO: better description */} + {field} + setValue('key', getValues('label')), validate: (value) => { if (value.trim().length === 0) return 'Required field'; if (!isAlphanumeric(value)) return 'Only alphanumeric characters are allowed'; @@ -87,6 +88,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 + From 13308dda2a261b442b7553dce532ade24113f14f Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 12 Oct 2024 12:25:45 +0200 Subject: [PATCH 3/8] also kep upper case when creating a new field --- apps/server/src/services/rundown-service/rundownCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index 96221435fd..14b20fd350 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -447,7 +447,7 @@ function scheduleCustomFieldPersist(persistedCustomFields: CustomFields) { */ export const createCustomField = async (field: CustomField) => { const { label, type, colour } = field; - const key = label.toLowerCase(); + const key = label; // check if label already exists const alreadyExists = Object.hasOwn(persistedCustomFields, key); From db1453e28cfedd1bb9888499e5399264f6b61dc2 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 12 Oct 2024 12:35:46 +0200 Subject: [PATCH 4/8] fix test --- .../__tests__/rundownCache.test.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts index aadab9a6b7..c6af0745b2 100644 --- a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts +++ b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts @@ -925,7 +925,7 @@ describe('custom fields', () => { 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); }); From 24c51638e943073db5ea7d95b7597fabf3ef2bc4 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 12 Oct 2024 12:42:29 +0200 Subject: [PATCH 5/8] allow old forced case keys to stay as is --- .../src/utils/__tests__/parserFunctions.test.ts | 15 +++++++++++++-- apps/server/src/utils/parserFunctions.ts | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/server/src/utils/__tests__/parserFunctions.test.ts b/apps/server/src/utils/__tests__/parserFunctions.test.ts index 6274770679..512c93c962 100644 --- a/apps/server/src/utils/__tests__/parserFunctions.test.ts +++ b/apps/server/src/utils/__tests__/parserFunctions.test.ts @@ -328,10 +328,21 @@ describe('sanitiseCustomFields()', () => { 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); diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts index 469e9605f6..5da951b1f9 100644 --- a/apps/server/src/utils/parserFunctions.ts +++ b/apps/server/src/utils/parserFunctions.ts @@ -305,12 +305,12 @@ 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; + const key = originalKey.toLocaleLowerCase() === field.label.toLocaleLowerCase() ? originalKey : field.label; if (key in newCustomFields) { continue; } From 81559b2890dea83d6934b2d659f2ddee7f96f113 Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sat, 12 Oct 2024 13:12:52 +0200 Subject: [PATCH 6/8] allow space --- .../custom-fields/CustomFieldEntry.tsx | 2 ++ .../custom-fields/CustomFieldForm.tsx | 6 +++--- .../custom-fields/customFields.validation.ts | 4 ++-- .../integration-service/OscIntegration.ts | 6 ++++-- .../src/services/rundown-service/rundownCache.ts | 14 +++++++++++--- .../src/utils/__tests__/parserFunctions.test.ts | 11 +++++++++++ apps/server/src/utils/parserFunctions.ts | 16 ++++++++++++++-- packages/utils/index.ts | 4 +++- .../customField-utils/customFieldLabelToKey.ts | 12 ++++++++++++ packages/utils/src/regex-utils/isAlphanumeric.ts | 9 +++++++++ 10 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 packages/utils/src/customField-utils/customFieldLabelToKey.ts 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 40e9f2e1be..e6e0f5e4ee 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 @@ -43,6 +43,8 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) { ); } + console.log(field) + return ( diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx index 4057d5c241..a81a518def 100644 --- a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx +++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Button, Input } from '@chakra-ui/react'; import { CustomField } from 'ontime-types'; -import { isAlphanumeric } from 'ontime-utils'; +import { isAlphanumericWithSpace } from 'ontime-utils'; import { maybeAxiosError } from '../../../../../common/api/utils'; import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect'; @@ -75,10 +75,10 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) { setValue('key', getValues('label')), + onChange: () => 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; }, 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 { const { label, type, colour } = field; - const key = label; + 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 { 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); + }); + it('filters invalid entries', () => { const customFields: CustomFields = { test: { label: 'test', type: 'string', colour: 'red' }, diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts index 5da951b1f9..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'; @@ -310,7 +316,13 @@ export function sanitiseCustomFields(data: object): CustomFields { continue; } - const key = originalKey.toLocaleLowerCase() === field.label.toLocaleLowerCase() ? originalKey : 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..5669928574 --- /dev/null +++ b/packages/utils/src/customField-utils/customFieldLabelToKey.ts @@ -0,0 +1,12 @@ +import { isAlphanumericWithSpace } from '../regex-utils/isAlphanumeric'; + +/** + * @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); +}; From 8a2214e1e7de96cfa3ae3ff25ffa30f533b4795a Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 13 Oct 2024 08:49:30 -0500 Subject: [PATCH 7/8] add extension to import --- packages/utils/src/customField-utils/customFieldLabelToKey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/customField-utils/customFieldLabelToKey.ts b/packages/utils/src/customField-utils/customFieldLabelToKey.ts index 5669928574..fd56e29dc0 100644 --- a/packages/utils/src/customField-utils/customFieldLabelToKey.ts +++ b/packages/utils/src/customField-utils/customFieldLabelToKey.ts @@ -1,4 +1,4 @@ -import { isAlphanumericWithSpace } from '../regex-utils/isAlphanumeric'; +import { isAlphanumericWithSpace } from '../regex-utils/isAlphanumeric.js'; /** * @description Transforms a Custom field label into a valid key or returns null if not possible From 24de0561e766c52ac62dc0444c9a727faa1ecee3 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 13 Oct 2024 08:50:30 -0500 Subject: [PATCH 8/8] supply initial key to CustomFieldForm --- .../custom-fields/CustomFieldEntry.tsx | 1 + .../feature-settings-panel/custom-fields/CustomFieldForm.tsx | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) 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 e6e0f5e4ee..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 @@ -37,6 +37,7 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) { onSubmit={handleEdit} initialColour={colour} initialLabel={label} + initialKey={field} /> diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx index a81a518def..25d48d4f17 100644 --- a/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx +++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx @@ -16,10 +16,11 @@ interface CustomFieldsFormProps { onCancel: () => 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 || '', key: initialLabel || '' }, + defaultValues: { label: initialLabel || '', colour: initialColour || '', key: initialKey || '' }, resetOptions: { keepDirtyValues: true, },