Expected time for views (#1798)

* refactor: use correct metadata in op view

* refactor: use correct metadata in backstage view

* remove uneeded test

* fix: correct offset colour

* refactor: use correct metadata in timline view

* refactor: schedule item dont pass the whole event

* chore: lint
This commit is contained in:
Alex Christoffer Rasmussen
2025-10-05 14:46:42 +02:00
parent 700948bfbb
commit 22559c59d4
14 changed files with 263 additions and 263 deletions
+9 -11
View File
@@ -8,7 +8,7 @@ import { useSelectedEventId } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import useCustomFields from '../../common/hooks-query/useCustomFields';
import useProjectData from '../../common/hooks-query/useProjectData';
import useRundown from '../../common/hooks-query/useRundown';
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
import useSettings from '../../common/hooks-query/useSettings';
import { cx } from '../../common/utils/styleUtils';
import { throttle } from '../../common/utils/throttle';
@@ -21,14 +21,14 @@ import OperatorGroup from './operator-group/OperatorGroup';
import StatusBar from './status-bar/StatusBar';
import { getOperatorOptions, useOperatorOptions } from './operator.options';
import type { EditEvent } from './operator.types';
import { getEventData, makeOperatorMetadata } from './operator.utils';
import { getEventData } from './operator.utils';
import style from './Operator.module.scss';
const selectedOffset = 50;
export default function Operator() {
const { data, status } = useRundown();
const { data, rundownMetadata, status } = useRundownWithMetadata();
const { data: customFields, status: customFieldStatus } = useCustomFields();
const { data: projectData, status: projectDataStatus } = useProjectData();
@@ -113,7 +113,6 @@ export default function Operator() {
}
const canEdit = shouldEdit && subscribe.length;
const { process } = makeOperatorMetadata(selectedEventId);
return (
<div className={style.operatorContainer} data-testid='operator-view'>
@@ -130,8 +129,7 @@ export default function Operator() {
{data.order.map((entryId) => {
const entry = data.entries[entryId];
if (isOntimeEvent(entry)) {
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(entry);
const { isPast, isLinkedToLoaded, isLoaded, totalGap } = rundownMetadata[entryId];
// hide past events (if setting) and skipped events
if ((hidePast && isPast) || entry.skip) {
return null;
@@ -158,9 +156,9 @@ export default function Operator() {
delay={entry.delay}
dayOffset={entry.dayOffset}
isLinkedToLoaded={isLinkedToLoaded}
isSelected={isSelected}
isSelected={isLoaded}
isPast={isPast}
selectedRef={isSelected ? selectedRef : undefined}
selectedRef={isLoaded ? selectedRef : undefined}
showStart={showStart}
subscribed={subscribedData}
totalGap={totalGap}
@@ -179,7 +177,7 @@ export default function Operator() {
return null;
}
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry);
const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[entryId];
// hide past events (if setting) and skipped events
if ((hidePast && isPast) || nestedEntry.skip) {
@@ -207,9 +205,9 @@ export default function Operator() {
delay={nestedEntry.delay}
dayOffset={nestedEntry.dayOffset}
isLinkedToLoaded={isLinkedToLoaded}
isSelected={isSelected}
isSelected={isLoaded}
isPast={isPast}
selectedRef={isSelected ? selectedRef : undefined}
selectedRef={isLoaded ? selectedRef : undefined}
showStart={showStart}
subscribed={subscribedData}
totalGap={totalGap}
@@ -1,83 +0,0 @@
import { OntimeEvent } from 'ontime-types';
import { makeOperatorMetadata } from '../operator.utils';
describe('makeOperatorMetadata()', () => {
it('should track past, selected states, gaps and linking', () => {
const event1 = { id: 'event1', gap: 5, linkStart: false } as OntimeEvent;
const event2 = { id: 'event2', gap: 10, linkStart: true } as OntimeEvent;
const event3 = { id: 'event3', gap: 15, linkStart: true } as OntimeEvent;
const { process } = makeOperatorMetadata('event2');
expect(process(event1)).toEqual({
isPast: true,
isSelected: false,
totalGap: 5,
isLinkedToLoaded: false,
});
expect(process(event2)).toEqual({
isPast: false,
isSelected: true,
totalGap: 15,
isLinkedToLoaded: false,
});
expect(process(event3)).toEqual({
isPast: false,
isSelected: false,
totalGap: 30,
isLinkedToLoaded: true,
});
});
it('should handle null selectedId', () => {
const event1 = { id: 'event1', gap: 5, linkStart: true } as OntimeEvent;
const event2 = { id: 'event2', gap: 10, linkStart: false } as OntimeEvent;
const event3 = { id: 'event3', gap: 15, linkStart: true } as OntimeEvent;
const { process } = makeOperatorMetadata(null);
expect(process(event1)).toEqual({
isPast: false,
isSelected: false,
totalGap: 5,
isLinkedToLoaded: true,
});
expect(process(event2)).toEqual({
isPast: false,
isSelected: false,
totalGap: 15,
isLinkedToLoaded: false,
});
expect(process(event3)).toEqual({
isPast: false,
isSelected: false,
totalGap: 30,
isLinkedToLoaded: true,
});
});
it('should break linking chain on countToEnd events', () => {
const event1 = { id: 'event1', gap: 5, linkStart: true, countToEnd: false } as OntimeEvent;
const event2 = { id: 'event2', gap: 10, linkStart: true, countToEnd: true } as OntimeEvent;
const event3 = { id: 'event3', gap: 15, linkStart: true, countToEnd: false } as OntimeEvent;
const { process } = makeOperatorMetadata(null);
expect(process(event1)).toEqual({
isPast: false,
isSelected: false,
totalGap: 5,
isLinkedToLoaded: true,
});
expect(process(event2)).toEqual({
isPast: false,
isSelected: false,
totalGap: 15,
isLinkedToLoaded: true,
});
expect(process(event3)).toEqual({
isPast: false,
isSelected: false,
totalGap: 30,
isLinkedToLoaded: false,
});
});
});
@@ -1,74 +1,33 @@
import { CustomFields, EntryId, MaybeString, OntimeEvent } from 'ontime-types';
import { getPropertyValue } from '../viewers/common/viewUtils';
import type { Subscribed } from './operator.types';
type OperatorMetadata = {
isLinkedToLoaded: boolean;
isPast: boolean;
isSelected: boolean;
totalGap: number;
};
export function makeOperatorMetadata(selectedId: EntryId | null) {
const hasSelection = Boolean(selectedId);
let hasSeenSelected = false;
let totalGap = 0;
/** if the event can link all the way back to the currently playing event */
let isLinkedToLoaded = false;
let previousEvent: OntimeEvent | null = null;
function process(event: OntimeEvent): Readonly<OperatorMetadata> {
const isSelected = event.id === selectedId;
if (isSelected) {
hasSeenSelected = true;
}
// is past if we havent yet seen the selected event
const isPast = hasSelection && !hasSeenSelected;
totalGap += event.gap;
if (!isPast && !isSelected) {
/**
* isLinkToLoaded is a chain value that we maintain until we
* a) find an unlinked event
* b) find a countToEnd event
*/
isLinkedToLoaded = event.linkStart && !previousEvent?.countToEnd;
}
previousEvent = event;
return { isPast, isSelected, totalGap, isLinkedToLoaded };
}
return { process };
}
export function getEventData(
event: OntimeEvent,
main: MaybeString,
secondary: MaybeString,
subscriptions: string[],
customFields: CustomFields,
) {
const mainField = main ? getPropertyValue(event, main) ?? '' : event.title;
const secondaryField = getPropertyValue(event, secondary) ?? '';
// remove subscriptions that are not in customFields
const sanitisedSubscriptions = subscriptions.filter((field) => Object.hasOwn(customFields, field));
const subscribedData = sanitisedSubscriptions.reduce<Subscribed>((acc, id) => {
const field = customFields[id];
if (field) {
acc.push({
id,
label: field.label,
colour: field.colour,
value: event.custom[id],
});
}
return acc;
}, []);
return { mainField, secondaryField, subscribedData };
}
import { CustomFields, MaybeString, OntimeEvent } from 'ontime-types';
import { getPropertyValue } from '../viewers/common/viewUtils';
import type { Subscribed } from './operator.types';
export function getEventData(
event: OntimeEvent,
main: MaybeString,
secondary: MaybeString,
subscriptions: string[],
customFields: CustomFields,
) {
const mainField = main ? getPropertyValue(event, main) ?? '' : event.title;
const secondaryField = getPropertyValue(event, secondary) ?? '';
// remove subscriptions that are not in customFields
const sanitisedSubscriptions = subscriptions.filter((field) => Object.hasOwn(customFields, field));
const subscribedData = sanitisedSubscriptions.reduce<Subscribed>((acc, id) => {
const field = customFields[id];
if (field) {
acc.push({
id,
label: field.label,
colour: field.colour,
value: event.custom[id],
});
}
return acc;
}, []);
return { mainField, secondaryField, subscribedData };
}