();
+ 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