Feat: unify view search params (#1947)

* feat: main src in backstage view

* feat: main src in timeline view

* feat: main src in countdown view

* feat: main src in studio view

* feat: hide past events in countdown view

* notes can not be set as main src

* feat: show group title as secondary src

* chore: spelling

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
Alex Christoffer Rasmussen
2026-01-27 20:42:07 +01:00
committed by GitHub
parent ec1890c275
commit 026e24bf84
28 changed files with 224 additions and 64 deletions
+5 -2
View File
@@ -6,6 +6,7 @@ import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
import { cx } from '../../common/utils/styleUtils';
import { getPropertyValue } from '../common/viewUtils';
import TimelineMarkers from './timeline-markers/TimelineMarkers';
import { useTimelineOptions } from './timeline.options';
@@ -24,7 +25,7 @@ interface TimelineProps {
export default memo(Timeline);
function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: TimelineProps) {
const { width: screenWidth } = useViewportSize();
const { hidePast, fixedSize } = useTimelineOptions();
const { mainSource, hidePast, fixedSize } = useTimelineOptions();
const selectedRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -86,6 +87,8 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
const position = positions[index];
if (!position) return null;
const displayTitle = getPropertyValue(event, mainSource ?? 'title') || event.title;
return (
<TimelineEntry
key={event.id}
@@ -100,7 +103,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
totalGap={event.totalGap}
isLinkedToLoaded={event.isLinkedToLoaded}
dayOffset={event.dayOffset}
title={event.title}
title={displayTitle}
cue={event.cue}
width={position.width}
/>
@@ -12,7 +12,7 @@ import Loader from '../common/loader/Loader';
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
import Timeline from './Timeline';
import { getTimelineOptions } from './timeline.options';
import { getTimelineOptions, useTimelineOptions } from './timeline.options';
import { getUpcomingEvents, useScopedRundown } from './timeline.utils';
import TimelineSections from './TimelineSections';
import { TimelineData, useTimelineData } from './useTimelineData';
@@ -35,8 +35,9 @@ export default function TimelinePageLoader() {
return <TimelinePage {...data} />;
}
function TimelinePage({ events, projectData, settings }: TimelineData) {
function TimelinePage({ events, customFields, projectData, settings }: TimelineData) {
const { selectedEventId } = useSelectedEventId();
const { mainSource } = useTimelineOptions();
// holds copy of the rundown with only relevant events
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId);
@@ -47,7 +48,7 @@ function TimelinePage({ events, projectData, settings }: TimelineData) {
// populate options
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const progressOptions = useMemo(() => getTimelineOptions(defaultFormat), [defaultFormat]);
const progressOptions = useMemo(() => getTimelineOptions(defaultFormat, customFields), [defaultFormat, customFields]);
return (
<div className='timeline' data-testid='timeline-view'>
@@ -58,7 +59,7 @@ function TimelinePage({ events, projectData, settings }: TimelineData) {
<TimelineClock />
</div>
<TimelineSections now={now} next={next} followedBy={followedBy} />
<TimelineSections now={now} next={next} followedBy={followedBy} mainSource={mainSource} />
<Timeline
firstStart={firstStart}
@@ -5,6 +5,7 @@ import { useExpectedStartData } from '../../common/hooks/useSocket';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
import { formatDuration, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
import { useTranslation } from '../../translation/TranslationProvider';
import { getPropertyValue } from '../common/viewUtils';
import TimelineSection from './timeline-section/TimelineSection';
@@ -12,17 +13,18 @@ interface TimelineSectionsProps {
now: ExtendedEntry<OntimeEvent> | null;
next: ExtendedEntry<OntimeEvent> | null;
followedBy: ExtendedEntry<OntimeEvent> | null;
mainSource: keyof OntimeEvent | null;
}
export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) {
export default function TimelineSections({ now, next, followedBy, mainSource }: TimelineSectionsProps) {
const { getLocalizedString } = useTranslation();
const state = useExpectedStartData();
// gather card data
const titleNow = now?.title ?? '-';
const titleNow = getPropertyValue(now, mainSource ?? 'title') ?? '-';
const dueText = getLocalizedString('timeline.due').toUpperCase();
const nextText = next !== null ? next.title : '-';
const followedByText = followedBy !== null ? followedBy.title : '-';
const nextText = getPropertyValue(next, mainSource ?? 'title') ?? '-';
const followedByText = getPropertyValue(followedBy, mainSource ?? 'title') ?? '-';
let nextStatus: string | undefined;
let followedByStatus: string | undefined;
@@ -1,15 +1,33 @@
import { use, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { CustomFields, OntimeEvent } 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 { 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): ViewOption[] => {
export const getTimelineOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]);
return [
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
{
title: OptionTitle.DataSources,
collapsible: true,
options: [
{
id: 'main',
title: 'Main text',
description: 'Select the data source for the main text',
type: 'option',
values: mainOptions,
defaultValue: 'title',
},
],
},
{
title: OptionTitle.ElementVisibility,
collapsible: true,
@@ -34,6 +52,7 @@ export const getTimelineOptions = (timeFormat: string): ViewOption[] => {
};
type TimelineOptions = {
mainSource: keyof OntimeEvent | null;
hidePast: boolean;
fixedSize: boolean;
};
@@ -47,6 +66,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key);
return {
mainSource: getValue('main') as keyof OntimeEvent | null,
hidePast: isStringBoolean(getValue('hidePast')),
fixedSize: isStringBoolean(getValue('fixedSize')),
};
@@ -1,5 +1,6 @@
import { OntimeEntry, ProjectData, Settings } from 'ontime-types';
import { CustomFields, OntimeEntry, ProjectData, Settings } from 'ontime-types';
import useCustomFields from '../../common/hooks-query/useCustomFields';
import useProjectData from '../../common/hooks-query/useProjectData';
import { useFlatRundownWithMetadata } from '../../common/hooks-query/useRundown';
import useSettings from '../../common/hooks-query/useSettings';
@@ -8,6 +9,7 @@ import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
export interface TimelineData {
events: ExtendedEntry<OntimeEntry>[];
customFields: CustomFields;
projectData: ProjectData;
settings: Settings;
}
@@ -17,13 +19,15 @@ export function useTimelineData(): ViewData<TimelineData> {
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
const { data: projectData, status: projectDataStatus } = useProjectData();
const { data: settings, status: settingsStatus } = useSettings();
const { data: customFields, status: customFieldsStatus } = useCustomFields();
return {
data: {
events: rundownData,
customFields,
projectData,
settings,
},
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus]),
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]),
};
}