From 657cc22b44ceb5150e7ff71fe65886f7f2090af4 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Thu, 17 Aug 2023 21:43:11 +0200 Subject: [PATCH] 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 --- apps/client/src/AppRouter.tsx | 3 +- apps/client/src/common/api/apiUtils.ts | 11 +- .../input/colour-input/SwatchSelect.tsx | 2 +- .../common/components/schedule/Schedule.tsx | 4 +- .../client/src/common/hooks/useEventAction.ts | 11 +- .../utils/__tests__/eventsManager.test.js | 536 ------------------ .../utils/__tests__/eventsManager.test.ts | 55 ++ .../common/utils/__tests__/getDelayTo.test.js | 56 -- apps/client/src/common/utils/aliases.ts | 2 +- apps/client/src/common/utils/eventsManager.ts | 168 +----- apps/client/src/common/utils/getDelayTo.js | 25 - apps/client/src/features/AliasWrapper.tsx | 5 +- .../src/features/cuesheet/cuesheetCols.tsx | 7 + apps/client/src/features/cuesheet/defaults.ts | 1 + .../src/features/editors/Editor.module.scss | 5 + .../event-editor/EventEditor.module.scss | 78 ++- .../src/features/event-editor/EventEditor.tsx | 60 +- .../composite/CountedTextArea.tsx | 6 +- .../composite/CountedTextInput.tsx | 24 +- .../composite/EventEditorDataLeft.tsx | 49 ++ .../composite/EventEditorDataRight.tsx | 33 ++ .../composite/EventEditorTitles.tsx | 50 -- .../src/features/menu/RundownMenu.module.scss | 2 +- .../src/features/rundown/Rundown.module.scss | 28 +- apps/client/src/features/rundown/Rundown.tsx | 52 +- .../src/features/rundown/RundownEntry.tsx | 23 +- .../event-block/EventBlock.module.scss | 12 + .../rundown/event-block/EventBlock.tsx | 8 +- .../quick-add-block/QuickAddBlock.module.scss | 16 +- .../features/viewers/backstage/Backstage.tsx | 5 +- .../features/viewers/countdown/Countdown.tsx | 3 +- .../features/viewers/studio/StudioClock.tsx | 15 +- .../__tests__/studioClock.utils.test.ts | 203 +++++++ .../viewers/studio/studioClock.utils.ts | 76 +++ .../src/classes/data-provider/DataProvider.ts | 10 +- .../src/classes/event-loader/EventLoader.ts | 10 + .../src/controllers/integrationController.ts | 22 + apps/server/src/models/eventsDefinition.ts | 2 +- apps/server/src/services/PlaybackService.ts | 29 + .../rundown-service/RundownService.ts | 43 +- .../__tests__/delayedRundown.utils.test.ts | 26 +- .../src/utils/__tests__/eventUtils.test.js | 54 -- ...ndomName.test.js => getRandomName.test.ts} | 0 .../{parser.test.js => parser.test.ts} | 110 +--- apps/server/src/utils/__tests__/time.test.js | 28 - apps/server/src/utils/__tests__/time.test.ts | 58 ++ .../__tests__/{url.test.js => url.test.ts} | 2 +- apps/server/src/utils/eventUtils.js | 25 - apps/server/src/utils/parser.ts | 49 +- apps/server/src/utils/parserFunctions.ts | 13 +- apps/server/src/utils/parserUtils.ts | 1 - apps/server/src/utils/time.ts | 5 +- .../src/definitions/core/OntimeEvent.type.ts | 1 + packages/types/src/index.ts | 81 +-- packages/utils/index.ts | 6 + packages/utils/package.json | 1 - packages/utils/src/cue-utils/cueUtils.test.ts | 160 ++++++ packages/utils/src/cue-utils/cueUtils.ts | 80 +++ .../src/rundown-utils/rundownUtils.test.ts | 79 ++- .../utils/src/rundown-utils/rundownUtils.ts | 95 +++- packages/utils/src/types/types.test.ts | 18 + packages/utils/src/types/types.ts | 11 + 62 files changed, 1363 insertions(+), 1290 deletions(-) delete mode 100644 apps/client/src/common/utils/__tests__/eventsManager.test.js create mode 100644 apps/client/src/common/utils/__tests__/eventsManager.test.ts delete mode 100644 apps/client/src/common/utils/__tests__/getDelayTo.test.js delete mode 100644 apps/client/src/common/utils/getDelayTo.js create mode 100644 apps/client/src/features/event-editor/composite/EventEditorDataLeft.tsx create mode 100644 apps/client/src/features/event-editor/composite/EventEditorDataRight.tsx delete mode 100644 apps/client/src/features/event-editor/composite/EventEditorTitles.tsx create mode 100644 apps/client/src/features/viewers/studio/__tests__/studioClock.utils.test.ts create mode 100644 apps/client/src/features/viewers/studio/studioClock.utils.ts delete mode 100644 apps/server/src/utils/__tests__/eventUtils.test.js rename apps/server/src/utils/__tests__/{getRandomName.test.js => getRandomName.test.ts} (100%) rename apps/server/src/utils/__tests__/{parser.test.js => parser.test.ts} (89%) delete mode 100644 apps/server/src/utils/__tests__/time.test.js create mode 100644 apps/server/src/utils/__tests__/time.test.ts rename apps/server/src/utils/__tests__/{url.test.js => url.test.ts} (95%) delete mode 100644 apps/server/src/utils/eventUtils.js create mode 100644 packages/utils/src/cue-utils/cueUtils.test.ts create mode 100644 packages/utils/src/cue-utils/cueUtils.ts create mode 100644 packages/utils/src/types/types.test.ts create mode 100644 packages/utils/src/types/types.ts diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 71eb86541..ca631d359 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -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')); diff --git a/apps/client/src/common/api/apiUtils.ts b/apps/client/src/common/api/apiUtils.ts index 5063a27e3..1c15707fc 100644 --- a/apps/client/src/common/api/apiUtils.ts +++ b/apps/client/src/common/api/apiUtils.ts @@ -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(), diff --git a/apps/client/src/common/components/input/colour-input/SwatchSelect.tsx b/apps/client/src/common/components/input/colour-input/SwatchSelect.tsx index 84c6f9a57..bbafefec5 100644 --- a/apps/client/src/common/components/input/colour-input/SwatchSelect.tsx +++ b/apps/client/src/common/components/input/colour-input/SwatchSelect.tsx @@ -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'; diff --git a/apps/client/src/common/components/schedule/Schedule.tsx b/apps/client/src/common/components/schedule/Schedule.tsx index 341909998..bd392650b 100644 --- a/apps/client/src/common/components/schedule/Schedule.tsx +++ b/apps/client/src/common/components/schedule/Schedule.tsx @@ -34,8 +34,8 @@ export default function Schedule({ className }: ScheduleProps) { { 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 () => { diff --git a/apps/client/src/common/utils/__tests__/eventsManager.test.js b/apps/client/src/common/utils/__tests__/eventsManager.test.js deleted file mode 100644 index a65f9853b..000000000 --- a/apps/client/src/common/utils/__tests__/eventsManager.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/apps/client/src/common/utils/__tests__/eventsManager.test.ts b/apps/client/src/common/utils/__tests__/eventsManager.test.ts new file mode 100644 index 000000000..140b9ff3d --- /dev/null +++ b/apps/client/src/common/utils/__tests__/eventsManager.test.ts @@ -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); + }); +}); diff --git a/apps/client/src/common/utils/__tests__/getDelayTo.test.js b/apps/client/src/common/utils/__tests__/getDelayTo.test.js deleted file mode 100644 index 8ba459818..000000000 --- a/apps/client/src/common/utils/__tests__/getDelayTo.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/apps/client/src/common/utils/aliases.ts b/apps/client/src/common/utils/aliases.ts index 4478501bb..0f9e1e4c6 100644 --- a/apps/client/src/common/utils/aliases.ts +++ b/apps/client/src/common/utils/aliases.ts @@ -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 diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index 1edcf9172..31296a13c 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -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 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; - } -} diff --git a/apps/client/src/common/utils/getDelayTo.js b/apps/client/src/common/utils/getDelayTo.js deleted file mode 100644 index a6802a0d4..000000000 --- a/apps/client/src/common/utils/getDelayTo.js +++ /dev/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; -} diff --git a/apps/client/src/features/AliasWrapper.tsx b/apps/client/src/features/AliasWrapper.tsx index b406e8b07..f6808edab 100644 --- a/apps/client/src/features/AliasWrapper.tsx +++ b/apps/client/src/features/AliasWrapper.tsx @@ -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 =

(Component: ComponentType

) => { return (props: Partial

) => { diff --git a/apps/client/src/features/cuesheet/cuesheetCols.tsx b/apps/client/src/features/cuesheet/cuesheetCols.tsx index 2061feba1..caca2c628 100644 --- a/apps/client/src/features/cuesheet/cuesheetCols.tsx +++ b/apps/client/src/features/cuesheet/cuesheetCols.tsx @@ -77,6 +77,13 @@ function MakeUserField({ getValue, row: { index }, column: { id }, table }: Cell export function makeCuesheetColumns(userFields?: UserFields): ColumnDef[] { return [ + { + accessorKey: 'cue', + id: 'cue', + header: 'Cue', + cell: (row) => row.getValue(), + size: 75, + }, { accessorKey: 'isPublic', id: 'isPublic', diff --git a/apps/client/src/features/cuesheet/defaults.ts b/apps/client/src/features/cuesheet/defaults.ts index 842592b7a..fd6aac5b9 100644 --- a/apps/client/src/features/cuesheet/defaults.ts +++ b/apps/client/src/features/cuesheet/defaults.ts @@ -5,6 +5,7 @@ import { OntimeEntryCommonKeys, OntimeEvent } from 'ontime-types'; */ export const defaultColumnOrder: OntimeEntryCommonKeys[] = [ 'isPublic', + 'cue', 'timeStart', 'timeEnd', 'duration', diff --git a/apps/client/src/features/editors/Editor.module.scss b/apps/client/src/features/editors/Editor.module.scss index 47b7c7c96..acdd490c0 100644 --- a/apps/client/src/features/editors/Editor.module.scss +++ b/apps/client/src/features/editors/Editor.module.scss @@ -215,6 +215,11 @@ $playback-width: 26rem; flex-direction: column; } + +.mainContainer > .rundown { + padding: 1rem 0; +} + .content { padding-top: 1.5rem; } diff --git a/apps/client/src/features/event-editor/EventEditor.module.scss b/apps/client/src/features/event-editor/EventEditor.module.scss index f759b40c0..9ea11948b 100644 --- a/apps/client/src/features/event-editor/EventEditor.module.scss +++ b/apps/client/src/features/event-editor/EventEditor.module.scss @@ -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% -} \ No newline at end of file +} diff --git a/apps/client/src/features/event-editor/EventEditor.tsx b/apps/client/src/features/event-editor/EventEditor.tsx index 24848c4e8..11e49edc1 100644 --- a/apps/client/src/features/event-editor/EventEditor.tsx +++ b/apps/client/src/features/event-editor/EventEditor.tsx @@ -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(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 Loading...; @@ -40,34 +47,35 @@ export default function EventEditor() { return (

-
- Event ID - - {event.id} - -
-
- {`/ontime/gotoid/${event.id}`} -
- + + handleSubmit={handleSubmit} + > + {event.id} + {`/ontime/gotoid/${event.id}`} + {`/ontime/gotocue/${event.cue}`} +
); } diff --git a/apps/client/src/features/event-editor/composite/CountedTextArea.tsx b/apps/client/src/features/event-editor/composite/CountedTextArea.tsx index 5fb0a5528..225e1e65a 100644 --- a/apps/client/src/features/event-editor/composite/CountedTextArea.tsx +++ b/apps/client/src/features/event-editor/composite/CountedTextArea.tsx @@ -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 (
- + {`${value.length} characters`}