mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 04:13:47 +00:00
feat: event cue (#473)
* feat: parse cue * refactor: get delay from backend * feat: add cue to UI * refactor: remove deprecated delay logic * refactor: extract studio clock specific logic * refactor: extract utilities * refactor: fix issue with missing key * style: prevent cue overflow * style: prevent whitespace wrap * feat: add support for cues in integrations
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import withData from './features/viewers/ViewWrapper';
|
||||
|
||||
import withAlias from './features/AliasWrapper';
|
||||
import withData from './features/viewers/ViewWrapper';
|
||||
|
||||
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
||||
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
|
||||
|
||||
@@ -6,9 +6,14 @@ import { addLog } from '../stores/logger';
|
||||
import { nowInMillis } from '../utils/time';
|
||||
|
||||
export function logAxiosError(prepend: string, error: unknown) {
|
||||
const message = axios.isAxiosError(error)
|
||||
? `${prepend} ${(error as AxiosError).response?.statusText ?? ''}: ${(error as AxiosError).response?.data ?? ''}`
|
||||
: `${prepend}: ${error}`;
|
||||
let message;
|
||||
if (axios.isAxiosError(error)) {
|
||||
const statusText = (error as AxiosError).response?.statusText ?? '';
|
||||
const data = (error as AxiosError).response?.data ?? '';
|
||||
message = `${prepend} ${statusText}: ${data}`;
|
||||
} else {
|
||||
message = `${prepend}: ${error}`;
|
||||
}
|
||||
|
||||
addLog({
|
||||
id: generateId(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { TitleActions } from '../../../../features/event-editor/composite/EventEditorTitles';
|
||||
import { TitleActions } from '../../../../features/event-editor/composite/EventEditorDataLeft';
|
||||
|
||||
import Swatch from './Swatch';
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ export default function Schedule({ className }: ScheduleProps) {
|
||||
<ScheduleItem
|
||||
key={event.id}
|
||||
selected={selectedState}
|
||||
timeStart={event.timeStart}
|
||||
timeEnd={event.timeEnd}
|
||||
timeStart={event.timeStart + (event?.delay ?? 0)}
|
||||
timeEnd={event.timeEnd + (event?.delay ?? 0)}
|
||||
title={event.title}
|
||||
colour={isBackstage ? event.colour : ''}
|
||||
backstageEvent={!event.isPublic}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
||||
import { swapOntimeEvents } from 'ontime-utils';
|
||||
import { getCueCandidate, swapOntimeEvents } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
||||
import { logAxiosError } from '../api/apiUtils';
|
||||
@@ -67,16 +67,20 @@ export const useEventAction = () => {
|
||||
after: options?.after,
|
||||
};
|
||||
|
||||
if (newEvent?.cue === undefined) {
|
||||
newEvent.cue = getCueCandidate(queryClient.getQueryData(RUNDOWN_TABLE) || [], options?.after);
|
||||
}
|
||||
|
||||
// hard coding duration value to be as expected for now
|
||||
// this until timeOptions gets implemented
|
||||
if (typeof newEvent?.timeStart !== 'undefined' && typeof newEvent.timeEnd !== 'undefined') {
|
||||
if (newEvent?.timeStart !== undefined && newEvent.timeEnd !== undefined) {
|
||||
newEvent.duration = Math.max(0, newEvent?.timeEnd - newEvent?.timeStart) || 0;
|
||||
}
|
||||
|
||||
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
||||
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
||||
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
||||
if (typeof previousEvent !== 'undefined' && previousEvent.type === 'event') {
|
||||
if (previousEvent !== undefined && previousEvent.type === 'event') {
|
||||
newEvent.timeStart = previousEvent.timeEnd;
|
||||
newEvent.timeEnd = previousEvent.timeEnd;
|
||||
}
|
||||
@@ -126,7 +130,6 @@ export const useEventAction = () => {
|
||||
onError: (_error, _newEvent, context) => {
|
||||
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
|
||||
},
|
||||
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: async () => {
|
||||
|
||||
@@ -1,536 +0,0 @@
|
||||
import { formatEventList, getEventsWithDelay, trimRundown } from '../eventsManager';
|
||||
|
||||
describe('getEventsWithDelay function', () => {
|
||||
test('with positive delays', () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
duration: 60000,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000,
|
||||
timeEnd: 720000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
duration: 900000,
|
||||
type: 'delay',
|
||||
revision: 0,
|
||||
id: 'a386',
|
||||
},
|
||||
{
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000,
|
||||
timeEnd: 38520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000,
|
||||
timeEnd: 45120000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'e6a1',
|
||||
},
|
||||
{
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000 + 60000,
|
||||
timeEnd: 35520000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000 + 60000,
|
||||
timeEnd: 720000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000 + 60000 + 900000,
|
||||
timeEnd: 38520000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000 + 60000 + 900000,
|
||||
timeEnd: 45120000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
test('with negative delays', () => {
|
||||
const testData = [
|
||||
{
|
||||
duration: -20,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 100,
|
||||
timeEnd: 200,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 80,
|
||||
timeEnd: 180,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventsWithDelay edge cases', () => {
|
||||
it('ensures time start cannot be below 0', () => {
|
||||
const testData = [
|
||||
{
|
||||
duration: -200,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
it('does not modify original array', () => {
|
||||
const testData = [
|
||||
{
|
||||
duration: 10,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
const expectedSafe = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
expect(getEventsWithDelay(expectedSafe)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('given an empty array', () => {
|
||||
const emptyArray = {
|
||||
test: [],
|
||||
expect: [],
|
||||
};
|
||||
|
||||
expect(getEventsWithDelay(emptyArray.test)).toStrictEqual(emptyArray.expect);
|
||||
});
|
||||
|
||||
it('given an undefined object', () => {
|
||||
const withUndefined = {
|
||||
test: undefined,
|
||||
expect: [],
|
||||
};
|
||||
|
||||
expect(getEventsWithDelay(withUndefined.test)).toStrictEqual(withUndefined.expect);
|
||||
});
|
||||
|
||||
it('given a corrupted event object', () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
duration: 60000,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000 + 60000,
|
||||
timeEnd: 35520000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('given a corrupted delay object', () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test trimEventlist function', () => {
|
||||
const limit = 8;
|
||||
const testData = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
{ id: '10' },
|
||||
{ id: '11' },
|
||||
{ id: '12' },
|
||||
];
|
||||
|
||||
it('when we use the first item', () => {
|
||||
const selectedId = '1';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('when we use the third item', () => {
|
||||
const selectedId = '3';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('when we use the fourth item', () => {
|
||||
const selectedId = '4';
|
||||
const expected = [
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if selected is not found', () => {
|
||||
const selectedId = '15';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData, selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test formatEvents function', () => {
|
||||
const testEvent = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
subtitle: 'Subtitles are useful',
|
||||
presenter: 'cpvalente',
|
||||
note: 'Maybe a running note for the operator?',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: 'In green, below',
|
||||
timeStart: 34800000,
|
||||
timeEnd: 35400000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
it('it parses correctly', () => {
|
||||
const selectedId = 'otherEvent';
|
||||
const nextId = 'notHere';
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles selected correctly', () => {
|
||||
const selectedId = '5946';
|
||||
const nextId = '8ee5';
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: true,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles next correctly', () => {
|
||||
const selectedId = '8ee5';
|
||||
const nextId = 'notHere';
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { EndAction, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||
|
||||
import { cloneEvent } from '../eventsManager';
|
||||
|
||||
describe('cloneEvent()', () => {
|
||||
it('creates a stem from a given event', () => {
|
||||
const original = {
|
||||
id: 'unique',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'title',
|
||||
cue: 'cue',
|
||||
subtitle: 'subtitle',
|
||||
presenter: 'presenter',
|
||||
note: 'note',
|
||||
timeStart: 0,
|
||||
duration: 10,
|
||||
timeEnd: 10,
|
||||
timerType: TimerType.CountDown,
|
||||
endAction: EndAction.None,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: 'F00',
|
||||
revision: 10,
|
||||
user0: 'user0',
|
||||
user1: 'user1',
|
||||
user2: 'user2',
|
||||
user3: 'user3',
|
||||
user4: 'user4',
|
||||
user5: 'user5',
|
||||
user6: 'user6',
|
||||
user7: 'user7',
|
||||
user8: 'user8',
|
||||
user9: 'user9',
|
||||
} as OntimeEvent;
|
||||
|
||||
const cloned = cloneEvent(original);
|
||||
expect(cloned).not.toBe(original);
|
||||
// @ts-expect-error -- safeguarding this
|
||||
expect(cloned?.id).toBe(undefined);
|
||||
expect(cloned.title).toBe(original.title);
|
||||
expect(cloned.subtitle).toBe(original.subtitle);
|
||||
expect(cloned.presenter).toBe(original.presenter);
|
||||
expect(cloned.note).toBe(original.note);
|
||||
expect(cloned.endAction).toBe(original.endAction);
|
||||
expect(cloned.timerType).toBe(original.timerType);
|
||||
expect(cloned.timeStart).toBe(original.timeStart);
|
||||
expect(cloned.timeEnd).toBe(original.timeEnd);
|
||||
expect(cloned.duration).toBe(original.duration);
|
||||
expect(cloned.isPublic).toBe(original.isPublic);
|
||||
expect(cloned.skip).toBe(original.skip);
|
||||
expect(cloned.colour).toBe(original.colour);
|
||||
expect(cloned.type).toBe(SupportedEvent.Event);
|
||||
expect(cloned.revision).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import getDelayTo from '../getDelayTo';
|
||||
|
||||
describe('getDelayTo function', () => {
|
||||
it('handles list with delays', () => {
|
||||
const delayDuration = 100;
|
||||
const events = [
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: delayDuration },
|
||||
{ type: 'event' },
|
||||
];
|
||||
|
||||
const notDelayed = getDelayTo(events, 0);
|
||||
expect(notDelayed).toBe(0);
|
||||
const delayedEvent = getDelayTo(events, 2);
|
||||
expect(delayedEvent).toBe(delayDuration);
|
||||
});
|
||||
it('handles list without delays', () => {
|
||||
const events = [{ type: 'event' }, { type: 'event' }];
|
||||
const notDelayed = getDelayTo(events, 1);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
|
||||
it('handles list with multiple delays', () => {
|
||||
const delayDuration = 100;
|
||||
const events = [
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: delayDuration },
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: delayDuration },
|
||||
{ type: 'event' },
|
||||
];
|
||||
const doubleDelay = getDelayTo(events, 4);
|
||||
expect(doubleDelay).toBe(delayDuration * 2);
|
||||
});
|
||||
it('handles list with blocks', () => {
|
||||
const events = [
|
||||
{ type: 'event' },
|
||||
{ type: 'delay', duration: 100 },
|
||||
{ type: 'event' },
|
||||
{ type: 'block' },
|
||||
{ type: 'event' },
|
||||
];
|
||||
const notDelayed = getDelayTo(events, 4);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
it('handles index greater than list', () => {
|
||||
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
||||
const notDelayed = getDelayTo(events, 3);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
it('handles negative index (not found)', () => {
|
||||
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
||||
const notDelayed = getDelayTo(events, -1);
|
||||
expect(notDelayed).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Alias } from 'ontime-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { Location, resolvePath } from 'react-router-dom';
|
||||
import { Alias } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Validates an alias against defined parameters
|
||||
|
||||
@@ -1,174 +1,32 @@
|
||||
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { formatTime } from './time';
|
||||
|
||||
/**
|
||||
* @description From a list of events, returns only events of type event with calculated delays
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @returns {Object[]} Filtered events with calculated delays
|
||||
*/
|
||||
|
||||
export const getEventsWithDelay = (rundown: OntimeRundownEntry[]): OntimeEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
|
||||
const delayedEvents: OntimeEvent[] = [];
|
||||
|
||||
// Add running delay
|
||||
let delay = 0;
|
||||
for (const event of rundown) {
|
||||
if (event.type === SupportedEvent.Block) delay = 0;
|
||||
else if (event.type === SupportedEvent.Delay) {
|
||||
if (typeof event.duration === 'number') {
|
||||
delay += event.duration;
|
||||
}
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
const delayedEvent = { ...event };
|
||||
if (delay !== 0) {
|
||||
delayedEvent.timeStart = Math.max(delayedEvent.timeStart + delay, 0);
|
||||
delayedEvent.timeEnd = Math.max(delayedEvent.timeEnd + delay, 0);
|
||||
}
|
||||
delayedEvents.push(delayedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return delayedEvents;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Returns trimmed event list array
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {number} limit - max number of events to return
|
||||
* @returns {Object[]} Event list with maximum <limit> objects
|
||||
*/
|
||||
export const trimRundown = (rundown: OntimeEvent[], selectedId: string, limit: number): OntimeEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
|
||||
const BEFORE = 2;
|
||||
const trimmedRundown = [...rundown];
|
||||
|
||||
// limit events length if necessary
|
||||
if (limit != null) {
|
||||
while (trimmedRundown.length > limit) {
|
||||
const idx = trimmedRundown.findIndex((e) => e.id === selectedId);
|
||||
if (idx <= BEFORE) {
|
||||
trimmedRundown.pop();
|
||||
} else {
|
||||
trimmedRundown.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
return trimmedRundown;
|
||||
};
|
||||
|
||||
type FormatEventListOptionsProp = {
|
||||
showEnd?: boolean;
|
||||
};
|
||||
/**
|
||||
* @description Returns list of events formatted to be displayed
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {string} nextId - id of next event
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showEnd] - whether to show the end time
|
||||
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
|
||||
*/
|
||||
export const formatEventList = (
|
||||
rundown: OntimeEvent[],
|
||||
selectedId: string,
|
||||
nextId: string,
|
||||
options: FormatEventListOptionsProp,
|
||||
): ScheduleEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
const { showEnd = false } = options;
|
||||
|
||||
const givenEvents = [...rundown];
|
||||
|
||||
// format list
|
||||
const formattedEvents = [];
|
||||
for (const event of givenEvents) {
|
||||
const start = formatTime(event.timeStart);
|
||||
const end = formatTime(event.timeEnd);
|
||||
|
||||
formattedEvents.push({
|
||||
id: event.id,
|
||||
time: showEnd ? `${start} - ${end}` : start,
|
||||
title: event.title,
|
||||
isNow: event.id === selectedId,
|
||||
isNext: event.id === nextId,
|
||||
colour: event.colour,
|
||||
});
|
||||
}
|
||||
|
||||
return formattedEvents;
|
||||
};
|
||||
|
||||
export type ScheduleEvent = {
|
||||
id: string;
|
||||
time: string;
|
||||
title: string;
|
||||
isNow: boolean;
|
||||
isNext: boolean;
|
||||
colour: string;
|
||||
};
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* @description Creates a safe duplicate of an event
|
||||
* @param {object} event
|
||||
* @return {object} clean event
|
||||
* @param {OntimeEvent} event
|
||||
* @param {string} [after]
|
||||
* @return {OntimeEvent} clean event
|
||||
*/
|
||||
type ClonedEvent = OntimeEvent | { after?: string };
|
||||
type ClonedEvent = Omit<
|
||||
OntimeEvent,
|
||||
'id' | 'user0' | 'user1' | 'user2' | 'user3' | 'user4' | 'user5' | 'user6' | 'user7' | 'user8' | 'user9'
|
||||
>;
|
||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||
return {
|
||||
type: SupportedEvent.Event,
|
||||
title: event.title,
|
||||
cue: event.cue,
|
||||
subtitle: event.subtitle,
|
||||
presenter: event.presenter,
|
||||
note: event.note,
|
||||
timeStart: event.timeStart,
|
||||
duration: event.duration,
|
||||
timeEnd: event.timeEnd,
|
||||
timerType: event.timerType,
|
||||
endAction: event.endAction,
|
||||
isPublic: event.isPublic,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
after: after,
|
||||
revision: 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets first event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @return {OntimeEvent | null}
|
||||
*/
|
||||
export function getFirstEvent(rundown: OntimeRundownEntry[]) {
|
||||
return rundown.length ? rundown[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeEvent | null}
|
||||
*/
|
||||
export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string) {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index !== -1 && index + 1 < rundown.length) {
|
||||
return rundown[index + 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeEvent | null}
|
||||
*/
|
||||
export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: string) {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index !== -1 && index - 1 >= 0) {
|
||||
return rundown[index - 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* @description calculates delay to a given event
|
||||
* @param {array} events
|
||||
* @param {number} eventIndex
|
||||
* @return {number} - delay value of given event
|
||||
*/
|
||||
export default function getDelayTo(events, eventIndex) {
|
||||
let delay = 0;
|
||||
let index = 0;
|
||||
if (eventIndex >= 0) {
|
||||
for (const event of events) {
|
||||
if (eventIndex === index) {
|
||||
return delay;
|
||||
}
|
||||
|
||||
if (event.type === 'delay') {
|
||||
delay += event.duration;
|
||||
} else if (event.type === 'block') {
|
||||
delay = 0;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { ComponentType, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import useAliases from '../common/hooks-query/useAliases';
|
||||
import { getAliasRoute } from '../common/utils/aliases';
|
||||
import { ComponentType, useEffect } from 'react';
|
||||
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const withAlias = <P extends object>(Component: ComponentType<P>) => {
|
||||
return (props: Partial<P>) => {
|
||||
|
||||
@@ -77,6 +77,13 @@ function MakeUserField({ getValue, row: { index }, column: { id }, table }: Cell
|
||||
|
||||
export function makeCuesheetColumns(userFields?: UserFields): ColumnDef<OntimeRundownEntry>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'cue',
|
||||
id: 'cue',
|
||||
header: 'Cue',
|
||||
cell: (row) => row.getValue(),
|
||||
size: 75,
|
||||
},
|
||||
{
|
||||
accessorKey: 'isPublic',
|
||||
id: 'isPublic',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { OntimeEntryCommonKeys, OntimeEvent } from 'ontime-types';
|
||||
*/
|
||||
export const defaultColumnOrder: OntimeEntryCommonKeys[] = [
|
||||
'isPublic',
|
||||
'cue',
|
||||
'timeStart',
|
||||
'timeEnd',
|
||||
'duration',
|
||||
|
||||
@@ -215,6 +215,11 @@ $playback-width: 26rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.mainContainer > .rundown {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
gap: max(1rem, 2vh);
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'eventInfo eventActions'
|
||||
'timeOptions titles';
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-areas: 'time left right';
|
||||
grid-template-columns: auto 1fr 1fr;
|
||||
}
|
||||
|
||||
.timeOptions {
|
||||
grid-area: time;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
|
||||
.timers,
|
||||
.timeSettings {
|
||||
@@ -19,51 +23,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.eventInfo {
|
||||
grid-area: eventInfo;
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.eventId {
|
||||
margin-left:$element-spacing;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.eventActions {
|
||||
grid-area: eventActions;
|
||||
margin-left: auto;
|
||||
.left {
|
||||
grid-area: left;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
|
||||
.timeOptions {
|
||||
grid-area: timeOptions;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.titles {
|
||||
grid-area: titles;
|
||||
display: grid;
|
||||
grid-template-areas: 'left right';
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.left {
|
||||
grid-area: left;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
|
||||
.right {
|
||||
padding-left: 1rem;
|
||||
grid-area: right;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
.right {
|
||||
padding-left: 1rem;
|
||||
grid-area: right;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
|
||||
@mixin input-label() {
|
||||
@@ -94,6 +70,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.eventActions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 1.25rem;
|
||||
}
|
||||
@@ -104,6 +86,12 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.splitTwo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -116,4 +104,4 @@
|
||||
|
||||
.fullHeight {
|
||||
height: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import { useAppMode } from '../../common/stores/appModeStore';
|
||||
import getDelayTo from '../../common/utils/getDelayTo';
|
||||
|
||||
import EventEditorDataLeft from './composite/EventEditorDataLeft';
|
||||
import EventEditorDataRight from './composite/EventEditorDataRight';
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
import EventEditorTitles from './composite/EventEditorTitles';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
|
||||
export type EventEditorSubmitActions = keyof OntimeEvent;
|
||||
export type EditorUpdateFields = 'cue' | 'title' | 'presenter' | 'subtitle' | 'note' | 'colour';
|
||||
|
||||
export default function EventEditor() {
|
||||
const openId = useAppMode((state) => state.editId);
|
||||
const { data } = useRundown();
|
||||
const { updateEvent } = useEventAction();
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
const [delay, setDelay] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !openId) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventIndex = data.findIndex((event) => event.id === openId);
|
||||
if (eventIndex > -1) {
|
||||
const event = data[eventIndex];
|
||||
if (event.type === 'event') {
|
||||
setDelay(getDelayTo(data, eventIndex));
|
||||
setEvent(data[eventIndex] as OntimeEvent);
|
||||
}
|
||||
const event = data.find((event) => event.id === openId);
|
||||
if (event && event.type === SupportedEvent.Event) {
|
||||
setEvent(event as OntimeEvent);
|
||||
}
|
||||
}, [data, event, openId]);
|
||||
}, [data, openId]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: EditorUpdateFields, value: string) => {
|
||||
updateEvent({ id: event?.id, [field]: value });
|
||||
},
|
||||
[event?.id, updateEvent],
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
return <span>Loading...</span>;
|
||||
@@ -40,34 +47,35 @@ export default function EventEditor() {
|
||||
|
||||
return (
|
||||
<div className={style.eventEditor}>
|
||||
<div className={style.eventInfo}>
|
||||
Event ID
|
||||
<span className={style.eventId}>
|
||||
<CopyTag label={event.id}>{event.id}</CopyTag>
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.eventActions}>
|
||||
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
||||
</div>
|
||||
<EventEditorTimes
|
||||
eventId={event.id}
|
||||
timeStart={event.timeStart}
|
||||
timeEnd={event.timeEnd}
|
||||
duration={event.duration}
|
||||
delay={delay}
|
||||
delay={event.delay ?? 0}
|
||||
isPublic={event.isPublic}
|
||||
endAction={event.endAction}
|
||||
timerType={event.timerType}
|
||||
/>
|
||||
<EventEditorTitles
|
||||
key={event.id}
|
||||
<EventEditorDataLeft
|
||||
key={`${event.id}-left`}
|
||||
eventId={event.id}
|
||||
cue={event.cue}
|
||||
title={event.title}
|
||||
presenter={event.presenter}
|
||||
subtitle={event.subtitle}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<EventEditorDataRight
|
||||
key={`${event.id}-right`}
|
||||
note={event.note}
|
||||
colour={event.colour}
|
||||
/>
|
||||
handleSubmit={handleSubmit}
|
||||
>
|
||||
<CopyTag label='Event ID'>{event.id}</CopyTag>
|
||||
<CopyTag label='OSC trigger by id'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
||||
<CopyTag label='OSC trigger by cue'>{`/ontime/gotocue/${event.cue}`}</CopyTag>
|
||||
</EventEditorDataRight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Textarea } from '@chakra-ui/react';
|
||||
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
|
||||
import { TitleActions } from './EventEditorTitles';
|
||||
import { TitleActions } from './EventEditorDataLeft';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
@@ -24,7 +24,9 @@ export default function CountedTextArea(props: CountedTextAreaProps) {
|
||||
return (
|
||||
<div className={`${style.column} ${style.fullHeight}`}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
||||
<label className={style.inputLabel} htmlFor={field}>
|
||||
{label}
|
||||
</label>
|
||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||
</div>
|
||||
<Textarea
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { Input, InputProps } from '@chakra-ui/react';
|
||||
import { sanitiseCue } from 'ontime-utils';
|
||||
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
|
||||
import { TitleActions } from './EventEditorTitles';
|
||||
import { EditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface CountedTextInputProps {
|
||||
field: TitleActions;
|
||||
interface CountedTextInputProps extends InputProps {
|
||||
field: EditorUpdateFields;
|
||||
label: string;
|
||||
initialValue: string;
|
||||
submitHandler: (field: TitleActions, value: string) => void;
|
||||
submitHandler: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
const { field, label, initialValue, submitHandler } = props;
|
||||
const { field, label, initialValue, submitHandler, maxLength } = props;
|
||||
|
||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||
const submitCallback = useCallback(
|
||||
(newValue: string) => submitHandler(field, sanitiseCue(newValue)),
|
||||
[field, submitHandler],
|
||||
);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, {
|
||||
submitOnEnter: true,
|
||||
@@ -26,7 +29,9 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
||||
<label className={style.inputLabel} htmlFor={field}>
|
||||
{label}
|
||||
</label>
|
||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||
</div>
|
||||
<Input
|
||||
@@ -35,6 +40,7 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
variant='ontime-filled'
|
||||
data-testid='input-textfield'
|
||||
value={value}
|
||||
maxLength={maxLength || 50}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { memo } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
|
||||
import { type EditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import CountedTextInput from './CountedTextInput';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface EventEditorLeftProps {
|
||||
eventId: string;
|
||||
cue: string;
|
||||
title: string;
|
||||
presenter: string;
|
||||
subtitle: string;
|
||||
handleSubmit: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
const EventEditorDataLeft = (props: EventEditorLeftProps) => {
|
||||
const { eventId, cue, title, presenter, subtitle, handleSubmit } = props;
|
||||
|
||||
return (
|
||||
<div className={style.left}>
|
||||
<div className={style.splitTwo}>
|
||||
<div className={style.column}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor='eventId'>
|
||||
Event ID (read only)
|
||||
</label>
|
||||
</div>
|
||||
<Input
|
||||
id='eventId'
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
data-testid='input-textfield'
|
||||
value={eventId}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<CountedTextInput field='cue' label='Cue' initialValue={cue} submitHandler={handleSubmit} maxLength={10} />
|
||||
</div>
|
||||
<CountedTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
||||
<CountedTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
|
||||
<CountedTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventEditorDataLeft);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
|
||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||
import { EditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import CountedTextArea from './CountedTextArea';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface EventEditorRightProps {
|
||||
note: string;
|
||||
colour: string;
|
||||
handleSubmit: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
const EventEditorDataRight = (props: PropsWithChildren<EventEditorRightProps>) => {
|
||||
const { children, note, colour, handleSubmit } = props;
|
||||
|
||||
return (
|
||||
<div className={style.right}>
|
||||
<div className={style.column}>
|
||||
<label className={style.inputLabel}>Colour</label>
|
||||
<div className={style.inline}>
|
||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
<CountedTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
||||
<div className={style.eventActions}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventEditorDataRight);
|
||||
@@ -1,50 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
|
||||
import CountedTextArea from './CountedTextArea';
|
||||
import CountedTextInput from './CountedTextInput';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface EventEditorTitlesProps {
|
||||
eventId: string;
|
||||
title: string;
|
||||
presenter: string;
|
||||
subtitle: string;
|
||||
note: string;
|
||||
colour: string;
|
||||
}
|
||||
|
||||
export type TitleActions = 'title' | 'presenter' | 'subtitle' | 'note' | 'colour';
|
||||
|
||||
const EventEditorTitles = (props: EventEditorTitlesProps) => {
|
||||
const { eventId, title, presenter, subtitle, note, colour } = props;
|
||||
const { updateEvent } = useEventAction();
|
||||
|
||||
const handleSubmit = (field: TitleActions, value: string) => {
|
||||
updateEvent({ id: eventId, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.titles}>
|
||||
<div className={style.left}>
|
||||
<CountedTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
||||
<CountedTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
|
||||
<CountedTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
<div className={style.right}>
|
||||
<div className={style.column}>
|
||||
<label className={style.inputLabel}>Colour</label>
|
||||
<div className={style.inline}>
|
||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
<CountedTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventEditorTitles);
|
||||
@@ -1,4 +1,4 @@
|
||||
.headerButtons {
|
||||
text-align: right;
|
||||
padding-top: 24px;
|
||||
padding: 1.5rem 1rem 0 1rem;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
@use '../../theme/v2Styles' as *;
|
||||
|
||||
.eventContainer {
|
||||
flex: 1;
|
||||
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 4px 8px 0;
|
||||
padding: 0 4px;
|
||||
overflow-y: scroll;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
height: 100%;
|
||||
@@ -22,11 +26,29 @@
|
||||
.alignCenter {
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
|
||||
.spaceTop {
|
||||
margin-top: 24px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
min-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
.entryWrapper {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entryIndex {
|
||||
text-align: right;
|
||||
min-width: 2em;
|
||||
color: $label-gray;
|
||||
font-size: calc(1rem - 3px);
|
||||
}
|
||||
|
||||
.entry {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { lazy, MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Fragment, lazy, MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
|
||||
import { getFirst, getNext, getPrevious } from 'ontime-utils';
|
||||
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||
import { cloneEvent, getFirstEvent, getNextEvent, getPreviousEvent } from '../../common/utils/eventsManager';
|
||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||
|
||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||
import RundownEmpty from './RundownEmpty';
|
||||
@@ -59,8 +60,7 @@ export default function Rundown(props: RundownProps) {
|
||||
if (type === 'clone') {
|
||||
const cursorEvent = entries.find((event) => event.id === cursor);
|
||||
if (cursorEvent?.type === SupportedEvent.Event) {
|
||||
const newEvent = cloneEvent(cursorEvent);
|
||||
newEvent.after = cursorEvent.id;
|
||||
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
||||
addEvent(newEvent);
|
||||
}
|
||||
} else if (type === SupportedEvent.Event) {
|
||||
@@ -93,7 +93,7 @@ export default function Rundown(props: RundownProps) {
|
||||
if (entries.length < 1) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = cursor == null ? getFirstEvent(entries) : getNextEvent(entries, cursor);
|
||||
const nextEvent = cursor == null ? getFirst(entries) : getNext(entries, cursor);
|
||||
if (nextEvent) {
|
||||
moveCursorTo(nextEvent.id, nextEvent.type === SupportedEvent.Event);
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export default function Rundown(props: RundownProps) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
||||
const previousEvent = cursor == null ? getFirstEvent(entries) : getPreviousEvent(entries, cursor);
|
||||
const previousEvent = cursor == null ? getFirst(entries) : getPrevious(entries, cursor);
|
||||
if (previousEvent) {
|
||||
moveCursorTo(previousEvent.id, previousEvent.type === SupportedEvent.Event);
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export default function Rundown(props: RundownProps) {
|
||||
let previousEnd = 0;
|
||||
let thisEnd = 0;
|
||||
let previousEventId: string | undefined;
|
||||
let eventIndex = -1;
|
||||
let eventIndex = 0;
|
||||
let isPast = Boolean(featureData?.selectedEventId);
|
||||
|
||||
return (
|
||||
@@ -216,7 +216,7 @@ export default function Rundown(props: RundownProps) {
|
||||
<div className={style.list}>
|
||||
{statefulEntries.map((entry, index) => {
|
||||
if (index === 0) {
|
||||
eventIndex = -1;
|
||||
eventIndex = 0;
|
||||
}
|
||||
if (entry.type === SupportedEvent.Event) {
|
||||
eventIndex++;
|
||||
@@ -233,21 +233,25 @@ export default function Rundown(props: RundownProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
||||
<RundownEntry
|
||||
type={entry.type}
|
||||
eventIndex={eventIndex}
|
||||
isPast={isPast}
|
||||
data={entry}
|
||||
selected={isSelected}
|
||||
hasCursor={hasCursor}
|
||||
next={isNext}
|
||||
previousEnd={previousEnd}
|
||||
previousEventId={previousEventId}
|
||||
playback={isSelected ? featureData.playback : undefined}
|
||||
isRolling={featureData.playback === Playback.Roll}
|
||||
disableEdit={isExtracted}
|
||||
/>
|
||||
<Fragment key={entry.id}>
|
||||
<div className={style.entryWrapper}>
|
||||
{entry.type === SupportedEvent.Event && <div className={style.entryIndex}>{eventIndex}</div>}
|
||||
<div className={style.entry} key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
||||
<RundownEntry
|
||||
type={entry.type}
|
||||
isPast={isPast}
|
||||
data={entry}
|
||||
selected={isSelected}
|
||||
hasCursor={hasCursor}
|
||||
next={isNext}
|
||||
previousEnd={previousEnd}
|
||||
previousEventId={previousEventId}
|
||||
playback={isSelected ? featureData.playback : undefined}
|
||||
isRolling={featureData.playback === Playback.Roll}
|
||||
disableEdit={isExtracted}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{((showQuickEntry && hasCursor) || isLast) && (
|
||||
<QuickAddBlock
|
||||
showKbd={hasCursor}
|
||||
@@ -257,7 +261,7 @@ export default function Rundown(props: RundownProps) {
|
||||
disableAddBlock={entry.type === SupportedEvent.Block}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
<div className={style.spacer} />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||
import { calculateDuration } from 'ontime-utils';
|
||||
import { calculateDuration, getCueCandidate } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN_TABLE } from '../../common/api/apiConstants';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
import { useAppMode } from '../../common/stores/appModeStore';
|
||||
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||
import { useEmitLog } from '../../common/stores/logger';
|
||||
@@ -16,7 +18,6 @@ export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'del
|
||||
|
||||
interface RundownEntryProps {
|
||||
type: SupportedEvent;
|
||||
eventIndex: number;
|
||||
isPast: boolean;
|
||||
data: OntimeRundownEntry;
|
||||
selected: boolean;
|
||||
@@ -30,19 +31,8 @@ interface RundownEntryProps {
|
||||
}
|
||||
|
||||
export default function RundownEntry(props: RundownEntryProps) {
|
||||
const {
|
||||
eventIndex,
|
||||
isPast,
|
||||
data,
|
||||
selected,
|
||||
hasCursor,
|
||||
next,
|
||||
previousEnd,
|
||||
previousEventId,
|
||||
playback,
|
||||
isRolling,
|
||||
disableEdit,
|
||||
} = props;
|
||||
const { isPast, data, selected, hasCursor, next, previousEnd, previousEventId, playback, isRolling, disableEdit } =
|
||||
props;
|
||||
const { emitError } = useEmitLog();
|
||||
const { addEvent, updateEvent, deleteEvent, swapEvents } = useEventAction();
|
||||
|
||||
@@ -110,6 +100,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
}
|
||||
case 'clone': {
|
||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
||||
newEvent.cue = getCueCandidate(ontimeQueryClient.getQueryData(RUNDOWN_TABLE) || [], data.id);
|
||||
addEvent(newEvent);
|
||||
break;
|
||||
}
|
||||
@@ -162,10 +153,10 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
if (data.type === SupportedEvent.Event) {
|
||||
return (
|
||||
<EventBlock
|
||||
cue={data.cue}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
duration={data.duration}
|
||||
eventIndex={eventIndex + 1}
|
||||
eventId={data.id}
|
||||
isPublic={data.isPublic}
|
||||
endAction={data.endAction}
|
||||
|
||||
@@ -90,6 +90,18 @@ $skip-opacity: 0.1;
|
||||
position: absolute;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.cue {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 5.5rem;
|
||||
text-align: center;
|
||||
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
rotate: -90deg;
|
||||
}
|
||||
}
|
||||
|
||||
.playbackActions {
|
||||
|
||||
@@ -20,10 +20,10 @@ import EventBlockInner from './EventBlockInner';
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
interface EventBlockProps {
|
||||
cue: string;
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
duration: number;
|
||||
eventIndex: number;
|
||||
eventId: string;
|
||||
isPublic: boolean;
|
||||
endAction: EndAction;
|
||||
@@ -54,11 +54,11 @@ interface EventBlockProps {
|
||||
|
||||
export default function EventBlock(props: EventBlockProps) {
|
||||
const {
|
||||
eventId,
|
||||
cue,
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
eventIndex,
|
||||
eventId,
|
||||
isPublic = true,
|
||||
endAction,
|
||||
timerType,
|
||||
@@ -187,7 +187,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
{eventIndex}
|
||||
<span className={style.cue}>{cue}</span>
|
||||
</div>
|
||||
{isVisible && (
|
||||
<EventBlockInner
|
||||
|
||||
@@ -4,26 +4,27 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
padding: 0 10px;
|
||||
margin: 0.25rem 0;
|
||||
font-size: $inner-section-text-size;
|
||||
padding: 0 0.75rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btnRow {
|
||||
justify-self: center;
|
||||
display: flex;
|
||||
gap: 10%;
|
||||
gap: max(0.5rem, 2rem);
|
||||
|
||||
.quickBtn {
|
||||
font-weight: 400;
|
||||
width: auto;
|
||||
padding: 0 32px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
margin-left: 8px;
|
||||
padding: 0 4px;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0 0.25rem;
|
||||
color: $label-gray;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
@@ -32,4 +33,5 @@
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||
import { EventData, Message, OntimeEvent, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
@@ -15,7 +15,6 @@ import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-edito
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { titleVariants } from '../common/animation';
|
||||
@@ -74,7 +73,7 @@ export default function Backstage(props: BackstageProps) {
|
||||
: formatTime(time.expectedFinish, formatOptions);
|
||||
|
||||
const qrSize = Math.max(window.innerWidth / 15, 128);
|
||||
const filteredEvents = getEventsWithDelay(backstageEvents);
|
||||
const filteredEvents = backstageEvents.filter((event) => event.type === SupportedEvent.Event);
|
||||
const showPublicMessage = publ.text && publ.visible;
|
||||
const showProgress = time.playback !== 'stop';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-edito
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import getDelayTo from '../../../common/utils/getDelayTo';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
|
||||
@@ -72,7 +71,7 @@ export default function Countdown(props: CountdownProps) {
|
||||
if (followThis !== null) {
|
||||
setFollow(followThis);
|
||||
const idx: number = backstageEvents.findIndex((event: OntimeRundownEntry) => event.id === followThis?.id);
|
||||
const delayToEvent = getDelayTo(backstageEvents, idx);
|
||||
const delayToEvent = backstageEvents[idx]?.delay ?? 0;
|
||||
setDelay(delayToEvent);
|
||||
}
|
||||
}, [backstageEvents, searchParams]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type { OntimeRundown, ViewSettings } from 'ontime-types';
|
||||
import type { OntimeEvent, OntimeRundown, ViewSettings } from 'ontime-types';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
@@ -11,15 +12,11 @@ import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { secondsInMillis } from '../../../common/utils/dateConfig';
|
||||
import {
|
||||
formatEventList,
|
||||
getEventsWithDelay,
|
||||
type ScheduleEvent,
|
||||
trimRundown,
|
||||
} from '../../../common/utils/eventsManager';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import { type ScheduleEvent, formatEventList, trimRundown } from './studioClock.utils';
|
||||
|
||||
import './StudioClock.scss';
|
||||
|
||||
const formatOptions = {
|
||||
@@ -66,8 +63,8 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delayed = getEventsWithDelay(backstageEvents);
|
||||
const trimmed = trimRundown(delayed, selectedId || '', MAX_TITLES);
|
||||
const delayed = backstageEvents.filter((event) => event.type === SupportedEvent.Event);
|
||||
const trimmed = trimRundown(delayed as OntimeEvent[], selectedId || '', MAX_TITLES);
|
||||
|
||||
const formatted = formatEventList(trimmed, selectedId || '', nextId || '', {
|
||||
showEnd: false,
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { formatEventList, trimRundown } from '../studioClock.utils';
|
||||
|
||||
describe('test trimEventlist function', () => {
|
||||
const limit = 8;
|
||||
const testData = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
{ id: '10' },
|
||||
{ id: '11' },
|
||||
{ id: '12' },
|
||||
];
|
||||
|
||||
it('when we use the first item', () => {
|
||||
const selectedId = '1';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('when we use the third item', () => {
|
||||
const selectedId = '3';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('when we use the fourth item', () => {
|
||||
const selectedId = '4';
|
||||
const expected = [
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if selected is not found', () => {
|
||||
const selectedId = '15';
|
||||
const expected = [
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||
expect(l.length).toBe(limit);
|
||||
expect(l).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test formatEvents function', () => {
|
||||
const testEvent = [
|
||||
{
|
||||
title: 'Welcome to Ontime',
|
||||
subtitle: 'Subtitles are useful',
|
||||
presenter: 'cpvalente',
|
||||
note: 'Maybe a running note for the operator?',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
title: 'Unless recalled by the OSC address',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: 'In green, below',
|
||||
timeStart: 34800000,
|
||||
timeEnd: 35400000,
|
||||
isPublic: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
id: '8ee5',
|
||||
},
|
||||
];
|
||||
|
||||
it('it parses correctly', () => {
|
||||
const selectedId = 'otherEvent';
|
||||
const nextId = 'notHere';
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles selected correctly', () => {
|
||||
const selectedId = '5946';
|
||||
const nextId = '8ee5';
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: true,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('it handles next correctly', () => {
|
||||
const selectedId = '8ee5';
|
||||
const nextId = 'notHere';
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: '5946',
|
||||
time: '08:00 - 08:30',
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
time: '09:40 - 09:50',
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: '',
|
||||
},
|
||||
];
|
||||
|
||||
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
export type ScheduleEvent = {
|
||||
id: string;
|
||||
time: string;
|
||||
title: string;
|
||||
isNow: boolean;
|
||||
isNext: boolean;
|
||||
colour: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Returns trimmed event list array
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {number} limit - max number of events to return
|
||||
* @returns {Object[]} Event list with maximum <limit> objects
|
||||
*/
|
||||
export const trimRundown = (rundown: OntimeEvent[], selectedId: string, limit: number): OntimeEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
|
||||
const BEFORE = 2;
|
||||
const trimmedRundown = [...rundown];
|
||||
|
||||
// limit events length if necessary
|
||||
if (limit != null) {
|
||||
while (trimmedRundown.length > limit) {
|
||||
const idx = trimmedRundown.findIndex((e) => e.id === selectedId);
|
||||
if (idx <= BEFORE) {
|
||||
trimmedRundown.pop();
|
||||
} else {
|
||||
trimmedRundown.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
return trimmedRundown;
|
||||
};
|
||||
|
||||
type FormatEventListOptionsProp = {
|
||||
showEnd?: boolean;
|
||||
};
|
||||
/**
|
||||
* @description Returns list of events formatted to be displayed
|
||||
* @param {Object[]} rundown - given rundown
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {string} nextId - id of next event
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showEnd] - whether to show the end time
|
||||
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
|
||||
*/
|
||||
export const formatEventList = (
|
||||
rundown: OntimeEvent[],
|
||||
selectedId: string,
|
||||
nextId: string,
|
||||
options: FormatEventListOptionsProp,
|
||||
): ScheduleEvent[] => {
|
||||
if (rundown == null) return [];
|
||||
const { showEnd = false } = options;
|
||||
|
||||
// format list
|
||||
return rundown.map((event) => {
|
||||
const start = formatTime(event.timeStart + (event.delay || 0));
|
||||
const end = formatTime(event.timeEnd + (event.delay || 0));
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
time: showEnd ? `${start} - ${end}` : start,
|
||||
title: event.title,
|
||||
isNow: event.id === selectedId,
|
||||
isNext: event.id === nextId,
|
||||
colour: event.colour,
|
||||
};
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user