refactor(views): share the hook which reads view options

Six views carried a character identical hook to read their options from the
query and let a preset override them. The teleprompter added a seventh copy,
and then a second statement of the same precedence rule in the param mirror,
which has to know that a value a preset pins cannot be written back.

The rule now has one home and one name. Each view is left with the part which
is actually its own, its parser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
This commit is contained in:
Claude
2026-08-19 04:23:32 +00:00
parent 52f91357a2
commit b4e3e0e553
8 changed files with 43 additions and 73 deletions
@@ -0,0 +1,28 @@
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { PresetContext } from '../../context/PresetContext';
/**
* Reads a view's options from the URL.
*
* Every view configures itself from the query, and every view is also reachable
* through a URL preset. A preset pins the values it names: they take precedence
* over the query rather than seeding it, which is what makes a preset a fixed
* setup rather than a starting point. Views which let the operator change
* settings live have to know that, since a value the preset pins cannot be
* written back.
*
* @param parse the view's own parser. Called with the query, and with the
* preset's search string when one is in scope. Expected to be a module level
* function, so that identity does not change between renders.
*/
export function useViewOptions<T>(parse: (searchParams: URLSearchParams, presetParams?: URLSearchParams) => T): T {
const [searchParams] = useSearchParams();
const maybePreset = use(PresetContext);
return useMemo(() => {
const presetParams = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return parse(searchParams, presetParams);
}, [maybePreset, searchParams, parse]);
}
@@ -1,6 +1,4 @@
import { CustomFields, OntimeEvent, ProjectData } from 'ontime-types';
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import {
getTimeOption,
@@ -8,12 +6,12 @@ import {
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { useViewOptions } from '../../common/components/view-params-editor/useViewOptions';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import {
makeOptionsFromCustomFields,
makeProjectDataOptions,
} from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { getScheduleOptions } from '../common/schedule/schedule.options';
export const getBackstageOptions = (
@@ -97,13 +95,5 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
* Hook exposes the backstage view options
*/
export function useBackstageOptions(): BackstageOptions {
const [searchParams] = useSearchParams();
const maybePreset = use(PresetContext);
const options = useMemo(() => {
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return getOptionsFromParams(searchParams, defaultValues);
}, [maybePreset, searchParams]);
return options;
return useViewOptions(getOptionsFromParams);
}
@@ -1,6 +1,4 @@
import { CustomFields, EntryId, OntimeEvent } from 'ontime-types';
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import {
getTimeOption,
@@ -8,9 +6,9 @@ import {
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { useViewOptions } from '../../common/components/view-params-editor/useViewOptions';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { isStringBoolean } from '../common/viewUtils';
export const getCountdownOptions = (
@@ -127,13 +125,5 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
* Hook exposes the countdown view options
*/
export function useCountdownOptions(): CountdownOptions {
const [searchParams] = useSearchParams();
const maybePreset = use(PresetContext);
const options = useMemo(() => {
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return getOptionsFromParams(searchParams, defaultValues);
}, [maybePreset, searchParams]);
return options;
return useViewOptions(getOptionsFromParams);
}
+2 -12
View File
@@ -1,6 +1,4 @@
import { CustomFields, OntimeEvent } from 'ontime-types';
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import {
getTimeOption,
@@ -8,9 +6,9 @@ import {
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { useViewOptions } from '../../common/components/view-params-editor/useViewOptions';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { isStringBoolean } from '../common/viewUtils';
export const getStudioOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
@@ -72,13 +70,5 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
* Hook exposes the backstage view options
*/
export function useStudioOptions(): StudioOptions {
const [searchParams] = useSearchParams();
const maybePreset = use(PresetContext);
const options = useMemo(() => {
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return getOptionsFromParams(searchParams, defaultValues);
}, [maybePreset, searchParams]);
return options;
return useViewOptions(getOptionsFromParams);
}
@@ -1,11 +1,9 @@
import type { CustomFields } from 'ontime-types';
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { useViewOptions } from '../../common/components/view-params-editor/useViewOptions';
import type { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { isStringBoolean } from '../common/viewUtils';
import { DEFAULT_SPEED, MAX_FONT_SIZE, MAX_SPEED, MIN_FONT_SIZE, MIN_SPEED } from './teleprompter.scroll';
import type { HeadingSource, TeleprompterOptions } from './teleprompter.types';
@@ -248,11 +246,5 @@ export function getOptionsFromParams(
* Hook exposes the teleprompter view options
*/
export function useTeleprompterOptions(): TeleprompterOptions {
const [searchParams] = useSearchParams();
const maybePreset = use(PresetContext);
return useMemo(() => {
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return getOptionsFromParams(searchParams, defaultValues);
}, [maybePreset, searchParams]);
return useViewOptions(getOptionsFromParams);
}
@@ -52,9 +52,9 @@ export function useMirrorLiveParams(live: Record<string, string | null>) {
useEffect(() => {
/**
* A preset's own search string wins over the query when the options are
* read, so a mirrored param would show a value the next load would ignore.
* Better to leave the address bar alone than to write a link which lies.
* A preset pins the values it names, as useViewOptions describes, so a
* mirrored param would show a value the next load would ignore. Better to
* leave the address bar alone than to write a link which lies.
*/
if (isPreset) return;
@@ -1,6 +1,4 @@
import { CustomFields, OntimeEvent } from 'ontime-types';
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import {
getTimeOption,
@@ -8,9 +6,9 @@ import {
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { useViewOptions } from '../../common/components/view-params-editor/useViewOptions';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { isStringBoolean } from '../common/viewUtils';
export const getTimelineOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
@@ -81,13 +79,5 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
* Hook exposes the timeline view options
*/
export function useTimelineOptions(): TimelineOptions {
const [searchParams] = useSearchParams();
const maybePreset = use(PresetContext);
const options = useMemo(() => {
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return getOptionsFromParams(searchParams, defaultValues);
}, [maybePreset, searchParams]);
return options;
return useViewOptions(getOptionsFromParams);
}
+2 -12
View File
@@ -1,7 +1,5 @@
import { CustomFields, OntimeEvent, TimerType } from 'ontime-types';
import { validateTimerType } from 'ontime-utils';
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import type { SelectOption } from '../../common/components/select/Select';
import {
@@ -12,9 +10,9 @@ import {
TimeOptions,
} from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { useViewOptions } from '../../common/components/view-params-editor/useViewOptions';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { isStringBoolean, makeColourString } from '../common/viewUtils';
// manually match the properties of TimerType excluding the None
@@ -239,13 +237,5 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
* Hook exposes the timer view options
*/
export function useTimerOptions(): TimerOptions {
const [searchParams] = useSearchParams();
const maybePreset = use(PresetContext);
const options = useMemo(() => {
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return getOptionsFromParams(searchParams, defaultValues);
}, [maybePreset, searchParams]);
return options;
return useViewOptions(getOptionsFromParams);
}