diff --git a/apps/client/src/common/models/OntimeSettings.ts b/apps/client/src/common/models/OntimeSettings.ts index 2ef016c05..a12319f64 100644 --- a/apps/client/src/common/models/OntimeSettings.ts +++ b/apps/client/src/common/models/OntimeSettings.ts @@ -1,5 +1,4 @@ import { Settings } from 'ontime-types'; -import { normaliseAuxTimerNames } from 'ontime-utils'; export const ontimePlaceholderSettings: Settings = { version: '4.0.0', @@ -7,5 +6,5 @@ export const ontimePlaceholderSettings: Settings = { operatorKey: null, timeFormat: '24', language: 'en', - auxTimerNames: normaliseAuxTimerNames(), + auxTimerNames: ['', '', ''], }; diff --git a/apps/client/src/common/utils/auxTimerUtils.ts b/apps/client/src/common/utils/auxTimerUtils.ts index 4acf8caf3..ba4ccd77f 100644 --- a/apps/client/src/common/utils/auxTimerUtils.ts +++ b/apps/client/src/common/utils/auxTimerUtils.ts @@ -1,9 +1,3 @@ -/** - * Resolves the display label for an aux timer. - * Falls back to the provided default when no custom name is set. - * @param name - the aux timer's custom name (from the runtime store) - * @param fallback - label to use when no custom name is set - */ export function getAuxTimerLabel(name: string | undefined, fallback: string): string { const custom = name?.trim(); return custom ? custom : fallback; diff --git a/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx index c7527d1ab..11f6413b4 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx @@ -51,21 +51,21 @@ export default function OntimeActionForm({ }} value={watch(`outputs.${index}.action`)} options={[ - { value: 'aux1-pause', label: 'Aux timer 1: pause' }, - { value: 'aux2-pause', label: 'Aux timer 2: pause' }, - { value: 'aux3-pause', label: 'Aux timer 3: pause' }, + { value: 'aux1-pause', label: 'Aux 1: pause' }, + { value: 'aux2-pause', label: 'Aux 2: pause' }, + { value: 'aux3-pause', label: 'Aux 3: pause' }, - { value: 'aux1-start', label: 'Aux timer 1: start' }, - { value: 'aux2-start', label: 'Aux timer 2: start' }, - { value: 'aux3-start', label: 'Aux timer 3: start' }, + { value: 'aux1-start', label: 'Aux 1: start' }, + { value: 'aux2-start', label: 'Aux 2: start' }, + { value: 'aux3-start', label: 'Aux 3: start' }, - { value: 'aux1-stop', label: 'Aux timer 1: stop' }, - { value: 'aux2-stop', label: 'Aux timer 2: stop' }, - { value: 'aux3-stop', label: 'Aux timer 3: stop' }, + { value: 'aux1-stop', label: 'Aux 1: stop' }, + { value: 'aux2-stop', label: 'Aux 2: stop' }, + { value: 'aux3-stop', label: 'Aux 3: stop' }, - { value: 'aux1-set', label: 'Aux timer 1: set' }, - { value: 'aux2-set', label: 'Aux timer 2: set' }, - { value: 'aux3-set', label: 'Aux timer 3: set' }, + { value: 'aux1-set', label: 'Aux 1: set' }, + { value: 'aux2-set', label: 'Aux 2: set' }, + { value: 'aux3-set', label: 'Aux 3: set' }, { value: 'playback-start', label: 'Playback: start' }, { value: 'playback-stop', label: 'Playback: stop' }, diff --git a/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx b/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx index cff483035..d6fdf4a6b 100644 --- a/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx +++ b/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx @@ -1,5 +1,5 @@ import { Settings } from 'ontime-types'; -import { auxTimerNameMaxLength, numberOfAuxTimers } from 'ontime-utils'; +import { auxTimerNameMaxLength } from 'ontime-utils'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; @@ -12,9 +12,6 @@ import useSettings from '../../../../common/hooks-query/useSettings'; import { preventEscape } from '../../../../common/utils/keyEvent'; import * as Panel from '../../panel-utils/PanelUtils'; -/** zero based index of each aux timer, used to address the auxTimerNames array */ -const auxTimerIndexes = Array.from({ length: numberOfAuxTimers }, (_, index) => index); - export default function AuxTimerSettings() { const { data, status, refetch } = useSettings(); const { @@ -30,7 +27,6 @@ export default function AuxTimerSettings() { }, }); - // update form if we get new data from server useEffect(() => { if (data) { reset(data); @@ -79,16 +75,18 @@ export default function AuxTimerSettings() { {errors.root?.message} - {auxTimerIndexes.map((index) => ( - - - - - ))} + + + + + + + + + + + + diff --git a/apps/client/src/features/control/playback/PlaybackControl.module.scss b/apps/client/src/features/control/playback/PlaybackControl.module.scss index 2d6ab5f0a..eee06cbab 100644 --- a/apps/client/src/features/control/playback/PlaybackControl.module.scss +++ b/apps/client/src/features/control/playback/PlaybackControl.module.scss @@ -8,6 +8,9 @@ align-items: center; justify-content: space-between; margin-top: 1rem; +} + +.label { font-size: $inner-section-text-size; color: $label-gray; } diff --git a/apps/client/src/features/control/playback/PlaybackControl.tsx b/apps/client/src/features/control/playback/PlaybackControl.tsx index bdc7065ef..8b69cdcb7 100644 --- a/apps/client/src/features/control/playback/PlaybackControl.tsx +++ b/apps/client/src/features/control/playback/PlaybackControl.tsx @@ -27,7 +27,7 @@ export default function PlaybackControl() { timerPhase={data.timerPhase} />
- Aux timers + Aux timers ): { const operatorKey = settings?.operatorKey; const timeFormat = settings?.timeFormat; const language = settings?.language; - const auxTimerNames = normaliseAuxTimerNames(settings?.auxTimerNames); + const auxTimerNames = sanitiseAuxTimerNames(settings?.auxTimerNames); const version = '4.5.0'; db.settings = { version, diff --git a/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts b/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts index 48906eb2e..aab720630 100644 --- a/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts +++ b/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts @@ -20,7 +20,7 @@ describe('parseSettings()', () => { }); }); - it('carries custom aux timer names through and normalises to a length-3 array', () => { + it('carries custom aux timer names through and pads to a length-3 array', () => { const result = parseSettings({ settings: { version: '1', auxTimerNames: ['Speaker'] } as unknown as Settings, }); @@ -35,7 +35,6 @@ describe('parseSettings()', () => { }); it('creates the aux timer names for project files made before the feature existed', () => { - // a settings object as found in a project file which predates aux timer naming const oldSettings = { version: '4.5.0', editorKey: null, @@ -47,7 +46,6 @@ describe('parseSettings()', () => { const result = parseSettings({ settings: oldSettings as Settings }); expect(result.auxTimerNames).toStrictEqual(['', '', '']); - // the rest of the settings are untouched expect(result).toMatchObject({ timeFormat: '24', language: 'en' }); }); }); diff --git a/apps/server/src/api-data/settings/settings.parser.ts b/apps/server/src/api-data/settings/settings.parser.ts index 3e19847f0..1e7d89e0c 100644 --- a/apps/server/src/api-data/settings/settings.parser.ts +++ b/apps/server/src/api-data/settings/settings.parser.ts @@ -1,5 +1,5 @@ import { DatabaseModel, Settings } from 'ontime-types'; -import { normaliseAuxTimerNames } from 'ontime-utils'; +import { sanitiseAuxTimerNames } from 'ontime-utils'; import { getPartialProject } from '../../models/dataModel.js'; @@ -23,7 +23,7 @@ export function parseSettings(data: Partial): Settings { operatorKey: data.settings.operatorKey ?? defaultSettings.operatorKey, timeFormat: data.settings.timeFormat ?? defaultSettings.timeFormat, language: data.settings.language ?? defaultSettings.language, - // property added in v4.6.0, older project files will not contain it - auxTimerNames: normaliseAuxTimerNames(data.settings.auxTimerNames), + // older project files predate this property + auxTimerNames: sanitiseAuxTimerNames(data.settings.auxTimerNames), }; } diff --git a/apps/server/src/api-data/settings/settings.validation.ts b/apps/server/src/api-data/settings/settings.validation.ts index eebac488e..e83ea679d 100644 --- a/apps/server/src/api-data/settings/settings.validation.ts +++ b/apps/server/src/api-data/settings/settings.validation.ts @@ -1,5 +1,5 @@ import { body } from 'express-validator'; -import { normaliseAuxTimerNames } from 'ontime-utils'; +import { sanitiseAuxTimerNames } from 'ontime-utils'; import { requestValidationFunction } from '../validation-utils/validationFunction.js'; @@ -28,11 +28,7 @@ export const validateSettings = [ pinValidator('operatorKey'), body('timeFormat').isString().isIn(['12', '24']).withMessage('Time format can only be "12" or "24"'), body('language').isString().trim().notEmpty(), - body('auxTimerNames') - .isArray() - .withMessage('auxTimerNames must be an array') - // normalise to a fixed length array of trimmed, length capped strings - .customSanitizer(normaliseAuxTimerNames), + body('auxTimerNames').isArray().withMessage('auxTimerNames must be an array').customSanitizer(sanitiseAuxTimerNames), requestValidationFunction, ]; diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 395a35cc0..a3560b2ce 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -5,7 +5,7 @@ import cookieParser from 'cookie-parser'; import cors from 'cors'; import express from 'express'; import { LogOrigin, SimpleDirection, SimplePlayback, runtimeStorePlaceholder } from 'ontime-types'; -import { normaliseAuxTimerNames } from 'ontime-utils'; +import { sanitiseAuxTimerNames } from 'ontime-utils'; import serverTiming from 'server-timing'; import { oscServer } from './adapters/OscAdapter.js'; @@ -206,7 +206,7 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb * Module initialises the services and provides initial payload for the store */ const state = getState(); - const auxTimerNames = normaliseAuxTimerNames(getDataProvider().getSettings().auxTimerNames); + const [auxName1, auxName2, auxName3] = sanitiseAuxTimerNames(getDataProvider().getSettings().auxTimerNames); eventStore.init({ clock: state.clock, timer: state.timer, @@ -222,27 +222,27 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb current: timerConfig.auxTimerDefault, playback: SimplePlayback.Stop, direction: SimpleDirection.CountDown, - name: auxTimerNames[0] ?? '', + name: auxName1, }, auxtimer2: { duration: timerConfig.auxTimerDefault, current: timerConfig.auxTimerDefault, playback: SimplePlayback.Stop, direction: SimpleDirection.CountDown, - name: auxTimerNames[1] ?? '', + name: auxName2, }, auxtimer3: { duration: timerConfig.auxTimerDefault, current: timerConfig.auxTimerDefault, playback: SimplePlayback.Stop, direction: SimpleDirection.CountDown, - name: auxTimerNames[2] ?? '', + name: auxName3, }, ping: 1, }); - // seed the aux timer names onto the running service so they persist across commands - auxTimerService.loadNames(auxTimerNames); + // AuxTimerService owns its own SimpleTimer instances, so the store above doesn't update them + auxTimerService.loadNames(getDataProvider().getSettings().auxTimerNames); // initialise message service messageService.init(eventStore.set, eventStore.get); diff --git a/apps/server/src/classes/simple-timer/SimpleTimer.ts b/apps/server/src/classes/simple-timer/SimpleTimer.ts index 8969b74ef..157e9c2a1 100644 --- a/apps/server/src/classes/simple-timer/SimpleTimer.ts +++ b/apps/server/src/classes/simple-timer/SimpleTimer.ts @@ -29,9 +29,6 @@ export class SimpleTimer { }; } - /** - * Sets the custom name of the timer - */ public setName(name: string): SimpleTimerState { this.state.name = name; return this.state; diff --git a/apps/server/src/models/dataModel.ts b/apps/server/src/models/dataModel.ts index 5bd188db8..e77c557d0 100644 --- a/apps/server/src/models/dataModel.ts +++ b/apps/server/src/models/dataModel.ts @@ -1,5 +1,5 @@ import { DatabaseModel, Rundown } from 'ontime-types'; -import { generateId, normaliseAuxTimerNames } from 'ontime-utils'; +import { generateId } from 'ontime-utils'; import { ONTIME_VERSION } from '../ONTIME_VERSION.js'; @@ -30,7 +30,7 @@ const dbModel: DatabaseModel = { operatorKey: null, timeFormat: '24', language: 'en', - auxTimerNames: normaliseAuxTimerNames(), + auxTimerNames: ['', '', ''], }, viewSettings: { overrideStyles: false, diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index 5dd5755d6..0bc1ad97c 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -1,5 +1,4 @@ import { DatabaseModel, OntimeView } from 'ontime-types'; -import { normaliseAuxTimerNames } from 'ontime-utils'; import { backstageRundown, broadcastRundown, stageRundown } from './demoRundowns.js'; @@ -30,7 +29,7 @@ export const demoDb: DatabaseModel = { operatorKey: null, timeFormat: '24', language: 'en', - auxTimerNames: normaliseAuxTimerNames(), + auxTimerNames: ['', '', ''], }, viewSettings: { dangerColor: '#ff7300', diff --git a/apps/server/src/services/aux-timer-service/AuxTimerService.ts b/apps/server/src/services/aux-timer-service/AuxTimerService.ts index affd60664..2437cabb4 100644 --- a/apps/server/src/services/aux-timer-service/AuxTimerService.ts +++ b/apps/server/src/services/aux-timer-service/AuxTimerService.ts @@ -1,5 +1,5 @@ import { RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types'; -import { normaliseAuxTimerNames } from 'ontime-utils'; +import { sanitiseAuxTimerNames } from 'ontime-utils'; import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js'; import { timerConfig } from '../../setup/config.js'; @@ -26,13 +26,11 @@ export class AuxTimerService { } /** - * Applies custom names to the aux timers and broadcasts the change. - * Names are given in aux timer order (index 0 is aux timer 1). - * Used to seed the names at bootstrap and to keep them in sync - * with the settings of the loaded project. + * Called at bootstrap and whenever the loaded project's settings change, + * so the running timers reflect the current project's aux timer names. */ loadNames(names?: string[]) { - const [name1, name2, name3] = normaliseAuxTimerNames(names); + const [name1, name2, name3] = sanitiseAuxTimerNames(names); const patch: AuxTimerStateUpdate = { auxtimer1: this.aux1.setName(name1), auxtimer2: this.aux2.setName(name2), diff --git a/apps/server/src/services/project-service/ProjectService.ts b/apps/server/src/services/project-service/ProjectService.ts index a2c01ae98..3c4a9b4a0 100644 --- a/apps/server/src/services/project-service/ProjectService.ts +++ b/apps/server/src/services/project-service/ProjectService.ts @@ -90,7 +90,7 @@ async function loadProject(projectData: DatabaseModel, fileName: string, rundown // stop the runtime service runtimeService.stop(); - // the aux timer names belong to the project, apply the ones from the newly loaded project + // AuxTimerService holds its own state, independent of the loaded project, so it needs to be updated explicitly auxTimerService.loadNames(projectData.settings.auxTimerNames); // load the rundown given by key otherwise load the first in the project @@ -352,7 +352,7 @@ export async function patchCurrentProject(data: Partial) { // we can pass some stuff straight to the data provider await getDataProvider().mergeIntoData(rest); - // the settings may contain new aux timer names, apply them and notify the clients + // AuxTimerService holds its own state, so a settings patch needs to be applied to it explicitly if (rest.settings) { auxTimerService.loadNames(getDataProvider().getSettings().auxTimerNames); sendRefetch(RefetchKey.Settings); diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 7f6b201b3..b0891f4fb 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -100,11 +100,7 @@ export { export { isPlaybackActive } from './src/playback-utils/playbackstate.js'; // aux timers -export { - auxTimerNameMaxLength, - normaliseAuxTimerNames, - numberOfAuxTimers, -} from './src/aux-timer-utils/auxTimerUtils.js'; +export { auxTimerNameMaxLength, sanitiseAuxTimerNames } from './src/aux-timer-utils/auxTimerUtils.js'; //Colour export { diff --git a/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts b/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts index e2e3d3947..eee892e3b 100644 --- a/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts +++ b/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts @@ -1,31 +1,30 @@ -import { auxTimerNameMaxLength, normaliseAuxTimerNames, numberOfAuxTimers } from './auxTimerUtils.js'; +import { auxTimerNameMaxLength, sanitiseAuxTimerNames } from './auxTimerUtils.js'; -describe('normaliseAuxTimerNames()', () => { +describe('sanitiseAuxTimerNames()', () => { it('generates the default value when given nothing', () => { - expect(normaliseAuxTimerNames()).toStrictEqual(['', '', '']); + expect(sanitiseAuxTimerNames()).toStrictEqual(['', '', '']); }); - it('always returns an entry per aux timer', () => { - expect(normaliseAuxTimerNames(['Speaker'])).toHaveLength(numberOfAuxTimers); - expect(normaliseAuxTimerNames(['a', 'b', 'c', 'extra'])).toStrictEqual(['a', 'b', 'c']); + it('always returns exactly three entries', () => { + expect(sanitiseAuxTimerNames(['a', 'b', 'c', 'extra'])).toStrictEqual(['a', 'b', 'c']); }); it('pads missing entries with an empty string', () => { - expect(normaliseAuxTimerNames(['Speaker'])).toStrictEqual(['Speaker', '', '']); + expect(sanitiseAuxTimerNames(['Speaker'])).toStrictEqual(['Speaker', '', '']); }); it('trims whitespace', () => { - expect(normaliseAuxTimerNames([' Speaker ', '', ''])).toStrictEqual(['Speaker', '', '']); + expect(sanitiseAuxTimerNames([' Speaker ', '', ''])).toStrictEqual(['Speaker', '', '']); }); it('caps the name length', () => { const tooLong = 'a'.repeat(auxTimerNameMaxLength + 10); - expect(normaliseAuxTimerNames([tooLong])[0]).toHaveLength(auxTimerNameMaxLength); + expect(sanitiseAuxTimerNames([tooLong])[0]).toHaveLength(auxTimerNameMaxLength); }); it('falls back to defaults for malformed data', () => { - expect(normaliseAuxTimerNames('not-an-array')).toStrictEqual(['', '', '']); - expect(normaliseAuxTimerNames(null)).toStrictEqual(['', '', '']); - expect(normaliseAuxTimerNames([42, {}, undefined])).toStrictEqual(['', '', '']); + expect(sanitiseAuxTimerNames('not-an-array')).toStrictEqual(['', '', '']); + expect(sanitiseAuxTimerNames(null)).toStrictEqual(['', '', '']); + expect(sanitiseAuxTimerNames([42, {}, undefined])).toStrictEqual(['', '', '']); }); }); diff --git a/packages/utils/src/aux-timer-utils/auxTimerUtils.ts b/packages/utils/src/aux-timer-utils/auxTimerUtils.ts index 19260634a..78d6b7ff4 100644 --- a/packages/utils/src/aux-timer-utils/auxTimerUtils.ts +++ b/packages/utils/src/aux-timer-utils/auxTimerUtils.ts @@ -1,22 +1,16 @@ -/** Number of aux timers available in ontime */ -export const numberOfAuxTimers = 3; - /** Maximum length of a user given aux timer name */ export const auxTimerNameMaxLength = 30; -/** - * Normalises user or file provided aux timer names into a - * fixed length array of trimmed, length capped strings. - * Missing or malformed entries fallback to an empty string, - * which consumers render as the default label. - * Used when parsing project files, when validating API payloads - * and to generate the default value. - */ -export function normaliseAuxTimerNames(maybeNames?: unknown): string[] { - const source = Array.isArray(maybeNames) ? maybeNames : []; - - return Array.from({ length: numberOfAuxTimers }, (_, index) => { - const value = source[index]; - return typeof value === 'string' ? value.trim().slice(0, auxTimerNameMaxLength) : ''; - }); +function sanitiseAuxTimerName(value: unknown): string { + return typeof value === 'string' ? value.trim().slice(0, auxTimerNameMaxLength) : ''; +} + +/** + * Ontime has three aux timers. Given whatever was found on disk or in a request body, + * returns a name for each of them, so callers never need to deal with a missing + * or malformed auxTimerNames (eg. a project file saved before this feature existed). + */ +export function sanitiseAuxTimerNames(names?: unknown): [string, string, string] { + const source = Array.isArray(names) ? names : []; + return [sanitiseAuxTimerName(source[0]), sanitiseAuxTimerName(source[1]), sanitiseAuxTimerName(source[2])]; } diff --git a/packages/utils/src/rundown-utils/rundownUtils.mock.ts b/packages/utils/src/rundown-utils/rundownUtils.mock.ts index 84e9b54cb..cab85e3f7 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.mock.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.mock.ts @@ -1,8 +1,6 @@ import type { DatabaseModel } from 'ontime-types'; import { EndAction, OntimeView, SupportedEntry, TimeStrategy, TimerType } from 'ontime-types'; -import { normaliseAuxTimerNames } from '../aux-timer-utils/auxTimerUtils.js'; - export const demoDb: DatabaseModel = { rundowns: { default: { @@ -344,7 +342,7 @@ export const demoDb: DatabaseModel = { operatorKey: null, timeFormat: '24', language: 'en', - auxTimerNames: normaliseAuxTimerNames(), + auxTimerNames: ['', '', ''], }, viewSettings: { dangerColor: '#ff7300',