diff --git a/apps/client/src/common/components/view-params-editor/ParamInput.tsx b/apps/client/src/common/components/view-params-editor/ParamInput.tsx index ffc93f231..960ebb944 100644 --- a/apps/client/src/common/components/view-params-editor/ParamInput.tsx +++ b/apps/client/src/common/components/view-params-editor/ParamInput.tsx @@ -18,19 +18,21 @@ import { import { isStringBoolean } from '../../../features/viewers/common/viewUtils'; import InlineColourPicker from './InlineColourPicker'; -import { ParamField } from './types'; +import { ParamField } from './viewParams.types'; -interface EditFormInputProps { +interface ParamInputProps { paramField: ParamField; } -export default function ParamInput(props: EditFormInputProps) { - const { paramField } = props; +export default function ParamInput({ paramField }: ParamInputProps) { const [searchParams] = useSearchParams(); const { id, type, defaultValue } = paramField; if (type === 'persist') { - return null; + if (!paramField.values || !paramField.values.length) { + return null; + } + return ; } if (type === 'option') { diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx index 0d10ad9fa..bd5e9b2c0 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx +++ b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx @@ -15,74 +15,12 @@ import { import useViewSettings from '../../hooks-query/useViewSettings'; import Info from '../info/Info'; -import { ViewOption } from './types'; +import { ViewOption } from './viewParams.types'; +import { getURLSearchParamsFromObj } from './viewParams.utils'; import ViewParamsSection from './ViewParamsSection'; import style from './ViewParamsEditor.module.scss'; -type ViewParamsObj = { [key: string]: string | FormDataEntryValue }; - -/** - * Utility remove the # character from a hex string - */ -function sanitiseColour(colour: string) { - if (colour.startsWith('#')) { - return colour.substring(1); - } - return colour; -} - -/** - * Makes a new URLSearchParams object from the given params object - */ -const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj, paramFields: ViewOption[]) => { - const newSearchParams = new URLSearchParams(); - - // Convert paramFields to an object that contains default values - const defaultValues: Record = {}; - paramFields.forEach((section) => { - section.options.forEach((option) => { - defaultValues[option.id] = String(option.defaultValue); - - // extract persisted values - if ('type' in option && option.type === 'persist') { - newSearchParams.set(option.id, option.value); - } - }); - }); - - // compare which values are different from the default values - Object.entries(paramsObj).forEach(([id, value]) => { - if (typeof value === 'string' && value.length) { - // we dont know which values contain colours - // unfortunately this means we run all the strings through the sanitation - const valueWithoutHash = sanitiseColour(value); - if (defaultValues[id] !== valueWithoutHash) { - handleValueString(id, valueWithoutHash); - } - } - }); - - /** Utility function contains logic to add a value into the searchParams object */ - function handleValueString(id: string, value: string) { - const maybeMultipleValues = value.split(','); - - // we need to check if the value contains comma separated list, for the case of the multi-select data - if (Array.isArray(maybeMultipleValues) && maybeMultipleValues.length > 1) { - const added = new Set(); - maybeMultipleValues.forEach((v) => { - if (!added.has(v)) { - added.add(v); - newSearchParams.append(id, v); - } - }); - } else { - newSearchParams.set(id, value); - } - } - return newSearchParams; -}; - interface EditFormDrawerProps { viewOptions: ViewOption[]; } @@ -94,6 +32,7 @@ export default function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) { const { isOpen, onClose, onOpen } = useDisclosure(); + // handle opening the drawer useEffect(() => { const isEditing = searchParams.get('edit'); diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx b/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx index 8ea23a5a0..275f95bb0 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx +++ b/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx @@ -3,8 +3,9 @@ import { useLocalStorage } from '@mantine/hooks'; import { cx } from '../../utils/styleUtils'; +import { OptionTitle } from './constants'; import ParamInput from './ParamInput'; -import { type ParamField } from './types'; +import { type ParamField } from './viewParams.types'; import style from './ViewParamsSection.module.scss'; @@ -14,9 +15,7 @@ interface ViewParamsSectionProps { options: ParamField[]; } -export default function ViewParamsSection(props: ViewParamsSectionProps) { - const { title, collapsible, options } = props; - +export default function ViewParamsSection({ title, collapsible, options }: ViewParamsSectionProps) { const [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false }); const handleCollapse = () => { @@ -27,28 +26,52 @@ export default function ViewParamsSection(props: ViewParamsSectionProps) { return (
-
- {title} - {collapsible && } -
- - {!collapsed && ( + {title === OptionTitle.Hidden ? ( + + ) : ( <> - {options.map((option) => { - if (option.type === 'persist') { - return null; - } - - return ( - - ); - })} +
+ {title} + {collapsible && } +
+ )}
); } + +interface SectionContentsProps { + options: ParamField[]; + collapsed: boolean; +} + +function SectionContents({ options, collapsed }: SectionContentsProps) { + if (collapsed) { + return null; + } + + return ( + <> + {options.map((option) => { + return ( + + ); + })} + + ); +} + +function HiddenContents({ options }: { options: ParamField[] }) { + return ( + <> + {options.map((option, index) => { + return ; + })} + + ); +} diff --git a/apps/client/src/common/components/view-params-editor/__tests__/constants.test.ts b/apps/client/src/common/components/view-params-editor/__tests__/constants.test.ts deleted file mode 100644 index 12659cb49..000000000 --- a/apps/client/src/common/components/view-params-editor/__tests__/constants.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { CustomFields } from 'ontime-types'; - -import { makeOptionsFromCustomFields } from '../constants'; - -describe('makeOptionsFromCustomFields', () => { - const testCustomFields: CustomFields = { - field1: { label: 'Field 1', colour: 'red', type: 'string' }, - field2: { label: 'Field 2', colour: 'blue', type: 'string' }, - }; - - it('creates a record of keys for the given custom fields', () => { - const result = makeOptionsFromCustomFields(testCustomFields); - expect(result).toStrictEqual({ - 'custom-field1': 'Custom: Field 1', - 'custom-field2': 'Custom: Field 2', - }); - }); - - it('appends additional data', () => { - const additionalData = { - test1: 'test1', - test2: 'test2', - }; - const result = makeOptionsFromCustomFields(testCustomFields, additionalData); - expect(result).toStrictEqual({ - 'custom-field1': 'Custom: Field 1', - 'custom-field2': 'Custom: Field 2', - test1: 'test1', - test2: 'test2', - }); - }); - - it('filtersImageTypes', () => { - const customFieldsWIthImage: CustomFields = { - ...testCustomFields, - field3: { label: 'Field 3', colour: 'green', type: 'image' }, - }; - - const result = makeOptionsFromCustomFields(customFieldsWIthImage); - expect(result).toStrictEqual({ - 'custom-field1': 'Custom: Field 1', - 'custom-field2': 'Custom: Field 2', - }); - }); -}); diff --git a/apps/client/src/common/components/view-params-editor/__tests__/viewParams.utils.test.ts b/apps/client/src/common/components/view-params-editor/__tests__/viewParams.utils.test.ts new file mode 100644 index 000000000..f164e7e89 --- /dev/null +++ b/apps/client/src/common/components/view-params-editor/__tests__/viewParams.utils.test.ts @@ -0,0 +1,204 @@ +import { CustomFields } from 'ontime-types'; +import { describe, expect, it } from 'vitest'; + +import { OptionTitle } from '../constants'; +import type { ViewOption } from '../viewParams.types'; +import { getURLSearchParamsFromObj, makeOptionsFromCustomFields } from '../viewParams.utils'; + +describe('makeOptionsFromCustomFields', () => { + const testCustomFields: CustomFields = { + field1: { label: 'Field 1', colour: 'red', type: 'string' }, + field2: { label: 'Field 2', colour: 'blue', type: 'string' }, + }; + + it('creates a record of keys for the given custom fields', () => { + const result = makeOptionsFromCustomFields(testCustomFields); + expect(result).toStrictEqual({ + 'custom-field1': 'Custom: Field 1', + 'custom-field2': 'Custom: Field 2', + }); + }); + + it('appends additional data', () => { + const additionalData = { + test1: 'test1', + test2: 'test2', + }; + const result = makeOptionsFromCustomFields(testCustomFields, additionalData); + expect(result).toStrictEqual({ + 'custom-field1': 'Custom: Field 1', + 'custom-field2': 'Custom: Field 2', + test1: 'test1', + test2: 'test2', + }); + }); + + it('filtersImageTypes', () => { + const customFieldsWIthImage: CustomFields = { + ...testCustomFields, + field3: { label: 'Field 3', colour: 'green', type: 'image' }, + }; + + const result = makeOptionsFromCustomFields(customFieldsWIthImage); + expect(result).toStrictEqual({ + 'custom-field1': 'Custom: Field 1', + 'custom-field2': 'Custom: Field 2', + }); + }); +}); + +describe('getURLSearchParamsFromObj', () => { + // Mock view options for testing + const mockViewOptions: ViewOption[] = [ + { + title: OptionTitle.DataSources, + collapsible: true, + options: [ + { + id: 'color', + title: 'Color', + description: 'The color value', + type: 'colour', + defaultValue: 'ff0000', + }, + { + id: 'persist-field', + title: 'Persistent Field', + description: 'A field that persists', + type: 'persist', + values: ['persisted-value'], + }, + { + id: 'multi-select', + title: 'Multi Select', + description: 'A multi-select field', + type: 'option', + values: { value1: 'Value 1', value2: 'Value 2' }, + defaultValue: '', + }, + ], + }, + ]; + + it('should handle empty params object', () => { + const params = {}; + const result = getURLSearchParamsFromObj(params, mockViewOptions); + + // Should only include persisted values + expect(result.get('persist-field')).toBe('persisted-value'); + expect(result.get('color')).toBeNull(); + }); + + it('should not include values that match defaults', () => { + const params = { + color: 'ff0000', // same as the default + 'other-param': 'value', + }; + const result = getURLSearchParamsFromObj(params, mockViewOptions); + + expect(result.get('color')).toBeNull(); + expect(result.get('other-param')).toBe('value'); + }); + + it('should sanitize color values with #', () => { + const params = { + color: '#00ff00', // different from default and includes # + }; + const result = getURLSearchParamsFromObj(params, mockViewOptions); + + expect(result.get('color')).toBe('00ff00'); + }); + + it('should handle multi-select values', () => { + const params = { + 'multi-select': 'value1,value2,value3', + }; + const result = getURLSearchParamsFromObj(params, mockViewOptions); + + // Should have multiple entries for the same key + const values = result.getAll('multi-select'); + expect(values).toEqual(['value1', 'value2', 'value3']); + }); + + it('should not include empty string values', () => { + const params = { + 'empty-param': '', + }; + const result = getURLSearchParamsFromObj(params, mockViewOptions); + expect(result.get('empty-param')).toBeNull(); + }); + + it('should allow multiple values for persisted fields', () => { + const mockOptionsWithMultiPersist: ViewOption[] = [ + { + title: OptionTitle.DataSources, + collapsible: true, + options: [ + { + id: 'sub', + title: 'Event subscription', + description: 'The events to follow', + values: [], + type: 'persist', + }, + ], + }, + ]; + + const params = { + sub: 'value1,value2,value3', + }; + const result = getURLSearchParamsFromObj(params, mockOptionsWithMultiPersist); + const values = result.getAll('sub'); + + expect(values).toStrictEqual(['value1', 'value2', 'value3']); + }); + + it('should allow adding multiple values for any field', () => { + const params = { + 'regular-field': 'value1,value2,value3', + }; + const result = getURLSearchParamsFromObj(params, mockViewOptions); + + const values = result.getAll('regular-field'); + expect(values).toHaveLength(3); + expect(values).toEqual(['value1', 'value2', 'value3']); + }); + + it('should deduplicate repeated key-value pairs', () => { + const params = { + multiValue: 'value1,value1,value2,value2,value3', + repeatedField: 'same,same,same', + }; + + const result = getURLSearchParamsFromObj(params, mockViewOptions); + + // Check multiValue field has unique values + expect(result.getAll('multiValue')).toEqual(['value1', 'value2', 'value3']); + + // Check repeatedField has only one instance of the value + expect(result.getAll('repeatedField')).toEqual(['same']); + }); + + it('should deduplicate persisted values while maintaining order', () => { + const mockOptionsWithDuplicates: ViewOption[] = [ + { + title: OptionTitle.StyleOverride, + options: [ + { + id: 'sub', + title: 'Subscription', + description: 'Persisted subscription values', + type: 'persist', + values: ['value1', 'value1', 'value2', 'value2', 'value3', 'value1'], + }, + ], + }, + ]; + + const result = getURLSearchParamsFromObj({}, mockOptionsWithDuplicates); + + // Should only include unique values while maintaining order + expect(result.getAll('sub')).toEqual(['value1', 'value2', 'value3']); + }); +}); diff --git a/apps/client/src/common/components/view-params-editor/common.options.ts b/apps/client/src/common/components/view-params-editor/common.options.ts new file mode 100644 index 000000000..6d1b72f6c --- /dev/null +++ b/apps/client/src/common/components/view-params-editor/common.options.ts @@ -0,0 +1,28 @@ +import type { ParamField } from './viewParams.types'; + +export const getTimeOption = (timeFormat: string): ParamField => { + const placeholder = `${timeFormat} (default)`; + return { + id: 'timeformat', + title: 'Time format string, taken from the Application Settings', + description: 'Format for auxiliar time fields (not the running), eg. HH:mm:ss or hh:mm:ss a, see docs for help', + type: 'string', + placeholder, + }; +}; + +export const hideTimerSeconds: ParamField = { + id: 'hideTimerSeconds', + title: 'Hide seconds in timer', + description: 'Whether to hide seconds in the running timer', + type: 'boolean', + defaultValue: false, +}; + +export const showLeadingZeros: ParamField = { + id: 'showLeadingZeros', + title: 'Show leading zeros in timer', + description: 'Whether to show leading zeros in the running timer', + type: 'boolean', + defaultValue: false, +}; diff --git a/apps/client/src/common/components/view-params-editor/constants.ts b/apps/client/src/common/components/view-params-editor/constants.ts index 6656f1da4..b9ba47d68 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -1,61 +1,6 @@ -import { CustomFields } from 'ontime-types'; - -import type { MultiselectOptions, ParamField } from './types'; - -export const makeOptionsFromCustomFields = ( - customFields: CustomFields, - additionalOptions: Readonly> = {}, - filterImageType = true, -) => { - const options = { ...additionalOptions }; - for (const [key, value] of Object.entries(customFields)) { - if (filterImageType && value.type === 'image') { - continue; - } - - options[`custom-${key}`] = `Custom: ${value.label}`; - } - return options; -}; - -export function makeCustomFieldSelectOptions(customFields: CustomFields, filterImageType = true): MultiselectOptions { - const options: MultiselectOptions = {}; - for (const [key, value] of Object.entries(customFields)) { - if (filterImageType && value.type === 'image') { - continue; - } - options[key] = { value: key, label: value.label, colour: value.colour }; - } - return options; -} - -export const getTimeOption = (timeFormat: string): ParamField => { - const placeholder = `${timeFormat} (default)`; - return { - id: 'timeformat', - title: 'Time format string, taken from the Application Settings', - description: 'Format for auxiliar time fields (not the running), eg. HH:mm:ss or hh:mm:ss a, see docs for help', - type: 'string', - placeholder, - }; -}; - -export const hideTimerSeconds: ParamField = { - id: 'hideTimerSeconds', - title: 'Hide seconds in timer', - description: 'Whether to hide seconds in the running timer', - type: 'boolean', - defaultValue: false, -}; - -export const showLeadingZeros: ParamField = { - id: 'showLeadingZeros', - title: 'Show leading zeros in timer', - description: 'Whether to show leading zeros in the running timer', - type: 'boolean', - defaultValue: false, -}; - +/** + * Gathers possible titles for view options + */ export enum OptionTitle { ClockOptions = 'Clock Options', TimerOptions = 'Timer Options', @@ -65,4 +10,7 @@ export enum OptionTitle { StyleOverride = 'View style override', Animation = 'View animation', Schedule = 'Schedule options', + + /** rendered as hidden inputs */ + Hidden = 'Hidden options', } diff --git a/apps/client/src/common/components/view-params-editor/types.ts b/apps/client/src/common/components/view-params-editor/viewParams.types.ts similarity index 92% rename from apps/client/src/common/components/view-params-editor/types.ts rename to apps/client/src/common/components/view-params-editor/viewParams.types.ts index 84ab3ff68..ee5d6e017 100644 --- a/apps/client/src/common/components/view-params-editor/types.ts +++ b/apps/client/src/common/components/view-params-editor/viewParams.types.ts @@ -24,7 +24,7 @@ type StringField = { type: 'string'; defaultValue?: string; prefix?: string; pla type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string }; type BooleanField = { type: 'boolean'; defaultValue: boolean }; type ColourField = { type: 'colour'; defaultValue: string; placeholder?: string }; -type PersistedField = { type: 'persist'; defaultValue?: string; value: string }; +type PersistedField = { type: 'persist'; defaultValue?: string[]; values: string[] }; export type ParamField = BaseField & (OptionsField | MultiOptionsField | StringField | NumberField | BooleanField | ColourField | PersistedField); diff --git a/apps/client/src/common/components/view-params-editor/viewParams.utils.ts b/apps/client/src/common/components/view-params-editor/viewParams.utils.ts new file mode 100644 index 000000000..4c5923529 --- /dev/null +++ b/apps/client/src/common/components/view-params-editor/viewParams.utils.ts @@ -0,0 +1,144 @@ +import type { CustomFields } from 'ontime-types'; + +import type { MultiselectOptions, ViewOption } from './viewParams.types'; + +/** + * Creates a list of custom fields for a select + * Filters out image type custom fields + */ +export function makeOptionsFromCustomFields( + customFields: CustomFields, + additionalOptions: Readonly> = {}, + filterImageType = true, +): Record { + const options = { ...additionalOptions }; + for (const [key, value] of Object.entries(customFields)) { + if (filterImageType && value.type === 'image') { + continue; + } + + options[`custom-${key}`] = `Custom: ${value.label}`; + } + return options; +} + +/** + * Creates data for a multiselect component from custom fields + * Filters out image type custom fields + */ +export function makeCustomFieldSelectOptions(customFields: CustomFields, filterImageType = true): MultiselectOptions { + const options: MultiselectOptions = {}; + for (const [key, value] of Object.entries(customFields)) { + if (filterImageType && value.type === 'image') { + continue; + } + options[key] = { value: key, label: value.label, colour: value.colour }; + } + return options; +} + +type ViewParamsObj = { [key: string]: string | FormDataEntryValue }; + +/** + * Utility remove the # character from a hex string + */ +function sanitiseColour(colour: string) { + if (colour.startsWith('#')) { + return colour.substring(1); + } + return colour; +} + +type FieldMetadata = { + defaultValues: Record; + colorFields: Set; + isPersistedField: Set; + persistedValues: Record; +}; + +/** + * Utility collects metadata about fields from view options + */ +function collectFieldMetadata(paramFields: ViewOption[]): FieldMetadata { + const metadata: FieldMetadata = { + defaultValues: {}, + colorFields: new Set(), + isPersistedField: new Set(), + persistedValues: {}, + }; + + paramFields.forEach((section) => { + section.options.forEach((option) => { + if (option.type === 'persist') { + metadata.isPersistedField.add(option.id); + if (option.values) { + metadata.persistedValues[option.id] = option.values; + } + } else { + metadata.defaultValues[option.id] = String(option.defaultValue); + } + + if (option.type === 'colour') { + metadata.colorFields.add(option.id); + } + }); + }); + + return metadata; +} + +/** + * Makes a new URLSearchParams object from the given params object + * @param paramsObj - The object containing parameters to be converted + * @param paramFields - The view options that define the parameters + * @returns A new URLSearchParams object with the parameters + */ +export function getURLSearchParamsFromObj(paramsObj: ViewParamsObj, paramFields: ViewOption[]) { + const newSearchParams = new URLSearchParams(); + const addedPairs = new Set(); + const metadata = collectFieldMetadata(paramFields); + + // Utility function to safely add params without duplicates + const addUniqueParam = (id: string, value: string) => { + const pair = `${id}:${value}`; + if (!addedPairs.has(pair)) { + addedPairs.add(pair); + newSearchParams.append(id, value); + } + }; + + // First add all persisted values + Object.entries(metadata.persistedValues).forEach(([id, values]) => { + values.forEach((value) => { + if (value) { + addUniqueParam(id, value); + } + }); + }); + + // Then process user-provided values + Object.entries(paramsObj).forEach(([id, value]) => { + if (typeof value === 'string' && value.length) { + // For persisted fields, clear existing values before adding new ones + if (metadata.isPersistedField.has(id)) { + // Clear tracking of previous values for this field + Array.from(addedPairs).forEach((pair) => { + if (pair.startsWith(`${id}:`)) { + addedPairs.delete(pair); + } + }); + newSearchParams.delete(id); + } + + // Process and add new values + value.split(',').forEach((v) => { + const processedValue = metadata.colorFields.has(id) ? sanitiseColour(v) : v; + if (metadata.isPersistedField.has(id) || metadata.defaultValues[id] !== processedValue) { + addUniqueParam(id, processedValue); + } + }); + } + }); + + return newSearchParams; +} diff --git a/apps/client/src/features/operator/operator.options.tsx b/apps/client/src/features/operator/operator.options.tsx index ca8e74a27..24ba81aef 100644 --- a/apps/client/src/features/operator/operator.options.tsx +++ b/apps/client/src/features/operator/operator.options.tsx @@ -1,12 +1,12 @@ import { CustomFields } from 'ontime-types'; +import { getTimeOption } from '../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; import { - getTimeOption, makeCustomFieldSelectOptions, makeOptionsFromCustomFields, - OptionTitle, -} from '../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../common/components/view-params-editor/types'; +} from '../../common/components/view-params-editor/viewParams.utils'; export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => { const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' }); diff --git a/apps/client/src/features/viewers/clock/clock.options.ts b/apps/client/src/features/viewers/clock/clock.options.ts index a00d89e1d..59cb05cf9 100644 --- a/apps/client/src/features/viewers/clock/clock.options.ts +++ b/apps/client/src/features/viewers/clock/clock.options.ts @@ -1,5 +1,6 @@ -import { getTimeOption, OptionTitle } from '../../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../../common/components/view-params-editor/types'; +import { getTimeOption } from '../../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types'; export const getClockOptions = (timeFormat: string): ViewOption[] => [ { title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] }, diff --git a/apps/client/src/features/viewers/lower-thirds/lowerThird.options.ts b/apps/client/src/features/viewers/lower-thirds/lowerThird.options.ts index fd3445cfa..93ba38b8d 100644 --- a/apps/client/src/features/viewers/lower-thirds/lowerThird.options.ts +++ b/apps/client/src/features/viewers/lower-thirds/lowerThird.options.ts @@ -2,8 +2,9 @@ import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { CustomFields } from 'ontime-types'; -import { makeOptionsFromCustomFields, OptionTitle } from '../../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../../common/components/view-params-editor/types'; +import { OptionTitle } from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types'; +import { makeOptionsFromCustomFields } from '../../../common/components/view-params-editor/viewParams.utils'; import safeParseNumber from '../../../common/utils/safeParseNumber'; export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] => { diff --git a/apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts b/apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts index 70abdc102..aa5fdaec0 100644 --- a/apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts +++ b/apps/client/src/features/viewers/minimal-timer/minimalTimer.options.ts @@ -1,9 +1,6 @@ -import { - hideTimerSeconds, - OptionTitle, - showLeadingZeros, -} from '../../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../../common/components/view-params-editor/types'; +import { hideTimerSeconds, showLeadingZeros } from '../../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types'; export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [ { title: OptionTitle.TimerOptions, collapsible: true, options: [hideTimerSeconds, showLeadingZeros] }, diff --git a/apps/client/src/features/viewers/studio/studioClock.options.ts b/apps/client/src/features/viewers/studio/studioClock.options.ts index c1353a91d..8c29eb0eb 100644 --- a/apps/client/src/features/viewers/studio/studioClock.options.ts +++ b/apps/client/src/features/viewers/studio/studioClock.options.ts @@ -1,5 +1,6 @@ -import { getTimeOption, hideTimerSeconds, OptionTitle } from '../../../common/components/view-params-editor/constants'; -import type { ViewOption } from '../../../common/components/view-params-editor/types'; +import { getTimeOption, hideTimerSeconds } from '../../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../../common/components/view-params-editor/constants'; +import type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types'; export const getStudioClockOptions = (timeFormat: string): ViewOption[] => [ { title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] }, diff --git a/apps/client/src/views/backstage/backstage.options.ts b/apps/client/src/views/backstage/backstage.options.ts index a0b2b414b..a16ec7e1f 100644 --- a/apps/client/src/views/backstage/backstage.options.ts +++ b/apps/client/src/views/backstage/backstage.options.ts @@ -2,12 +2,10 @@ import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { CustomFields, OntimeEvent } from 'ontime-types'; -import { - getTimeOption, - makeOptionsFromCustomFields, - OptionTitle, -} from '../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../common/components/view-params-editor/types'; +import { getTimeOption } from '../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; +import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { scheduleOptions } from '../common/schedule/schedule.options'; export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { diff --git a/apps/client/src/views/common/schedule/schedule.options.ts b/apps/client/src/views/common/schedule/schedule.options.ts index 58b3b531d..64540aa25 100644 --- a/apps/client/src/views/common/schedule/schedule.options.ts +++ b/apps/client/src/views/common/schedule/schedule.options.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { OptionTitle } from '../../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../../common/components/view-params-editor/types'; +import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types'; import { isStringBoolean } from '../../../features/viewers/common/viewUtils'; export const scheduleOptions: ViewOption = { diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index 4cacc18e4..69055f559 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -63,7 +63,7 @@ export default function Countdown({ // gather option data const defaultFormat = getDefaultFormat(settings?.timeFormat); - const countdownOptions = getCountdownOptions(defaultFormat, customFields); + const countdownOptions = getCountdownOptions(defaultFormat, customFields, subscriptions); return (
diff --git a/apps/client/src/views/countdown/countdown.options.ts b/apps/client/src/views/countdown/countdown.options.ts index 8a9f2d4a3..517929673 100644 --- a/apps/client/src/views/countdown/countdown.options.ts +++ b/apps/client/src/views/countdown/countdown.options.ts @@ -2,15 +2,17 @@ import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { CustomFields, EntryId, OntimeEvent } from 'ontime-types'; -import { - getTimeOption, - makeOptionsFromCustomFields, - OptionTitle, -} from '../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../common/components/view-params-editor/types'; +import { getTimeOption } from '../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; +import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { isStringBoolean } from '../../features/viewers/common/viewUtils'; -export const getCountdownOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { +export const getCountdownOptions = ( + timeFormat: string, + customFields: CustomFields, + persistedSubscriptions: EntryId[], +): ViewOption[] => { const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); return [ @@ -20,15 +22,6 @@ export const getCountdownOptions = (timeFormat: string, customFields: CustomFiel collapsible: true, options: [ { - id: 'sub', - title: 'Event subscription', - description: 'The events to follow', - value: '', - type: 'persist', - }, - { - // TODO: adding a secondary source is removing the subscriptions - // this seems to be a bug with persist assuming that the property has a single entry id: 'secondary-src', title: 'Event secondary text', description: 'Select the data source for auxiliary text shown in the card', @@ -51,6 +44,18 @@ export const getCountdownOptions = (timeFormat: string, customFields: CustomFiel }, ], }, + { + title: OptionTitle.Hidden, + options: [ + { + id: 'sub', + title: 'Event subscription', + description: 'The events to follow', + values: persistedSubscriptions, + type: 'persist', + }, + ], + }, ]; }; diff --git a/apps/client/src/views/cuesheet/cuesheet.options.ts b/apps/client/src/views/cuesheet/cuesheet.options.ts index 61e5309bd..d2a42a6ff 100644 --- a/apps/client/src/views/cuesheet/cuesheet.options.ts +++ b/apps/client/src/views/cuesheet/cuesheet.options.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { OptionTitle } from '../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../common/components/view-params-editor/types'; +import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; import { isStringBoolean } from '../../features/viewers/common/viewUtils'; /** diff --git a/apps/client/src/views/project-info/projectInfo.options.ts b/apps/client/src/views/project-info/projectInfo.options.ts index 8479b7e48..4243abdc3 100644 --- a/apps/client/src/views/project-info/projectInfo.options.ts +++ b/apps/client/src/views/project-info/projectInfo.options.ts @@ -1,5 +1,5 @@ import { OptionTitle } from '../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../common/components/view-params-editor/types'; +import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; export const projectInfoOptions: ViewOption[] = [ { diff --git a/apps/client/src/views/timeline/timeline.options.ts b/apps/client/src/views/timeline/timeline.options.ts index 541fda6a9..cd454f4a3 100644 --- a/apps/client/src/views/timeline/timeline.options.ts +++ b/apps/client/src/views/timeline/timeline.options.ts @@ -1,5 +1,6 @@ -import { getTimeOption, OptionTitle } from '../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../common/components/view-params-editor/types'; +import { getTimeOption } from '../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; export const getTimelineOptions = (timeFormat: string): ViewOption[] => { return [ diff --git a/apps/client/src/views/timer/timer.options.ts b/apps/client/src/views/timer/timer.options.ts index 32023b236..e2462d1c0 100644 --- a/apps/client/src/views/timer/timer.options.ts +++ b/apps/client/src/views/timer/timer.options.ts @@ -6,11 +6,11 @@ import { validateTimerType } from 'ontime-utils'; import { getTimeOption, hideTimerSeconds, - makeOptionsFromCustomFields, - OptionTitle, showLeadingZeros, -} from '../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../common/components/view-params-editor/types'; +} from '../../common/components/view-params-editor/common.options'; +import { OptionTitle } from '../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../common/components/view-params-editor/viewParams.types'; +import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { isStringBoolean } from '../../features/viewers/common/viewUtils'; // manually match the properties of TimerType excluding the None diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts new file mode 100644 index 000000000..e69de29bb