mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-23 07:59:10 +00:00
fix: apply delay (#494)
* fix: bug with applying delays * fix: show delay data only in production screens * chore: cover delay with feature test * refactor: simplify type assertion
This commit is contained in:
+26
-5
@@ -1,38 +1,48 @@
|
|||||||
# GETTING STARTED
|
# GETTING STARTED
|
||||||
|
|
||||||
Ontime consists of 3 distinct parts
|
Ontime consists of 3 distinct parts
|
||||||
|
|
||||||
- __client__: A React app for Ontime's UI and web clients
|
- __client__: A React app for Ontime's UI and web clients
|
||||||
- __electron__: An electron app which facilitates the cross-platform distribution of Ontime
|
- __electron__: An electron app which facilitates the cross-platform distribution of Ontime
|
||||||
- __server__: A node application which handles the domains services and integrations
|
- __server__: A node application which handles the domains services and integrations
|
||||||
|
|
||||||
The steps below will assume you have locally installed the necessary dependencies.
|
The steps below will assume you have locally installed the necessary dependencies.
|
||||||
Other dependencies will be installed as part of the setup
|
Other dependencies will be installed as part of the setup
|
||||||
|
|
||||||
- __node__ (>=16.16)
|
- __node__ (>=16.16)
|
||||||
- __pnpm__ (>=7)
|
- __pnpm__ (>=7)
|
||||||
- __docker__ (only necessary to run and build docker images)
|
- __docker__ (only necessary to run and build docker images)
|
||||||
|
|
||||||
## LOCAL DEVELOPMENT
|
## LOCAL DEVELOPMENT
|
||||||
|
|
||||||
The electron app is only necessary to distribute an installable version of the app and is not required for local development.
|
The electron app is only necessary to distribute an installable version of the app and is not required for local
|
||||||
|
development.
|
||||||
Locally, we would need to run both the React client and the node.js server in development mode
|
Locally, we would need to run both the React client and the node.js server in development mode
|
||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- __Run dev mode__ by running `turbo dev`
|
- __Run dev mode__ by running `turbo dev`
|
||||||
|
|
||||||
### Debugging backend
|
### Debugging backend
|
||||||
|
|
||||||
To debug backend code in Node.js:
|
To debug backend code in Node.js:
|
||||||
|
|
||||||
- Open two separate terminals and navigate to the `apps/client` and `apps/server` directories.
|
- Open two separate terminals and navigate to the `apps/client` and `apps/server` directories.
|
||||||
- In each terminal, run the command `pnpm dev` to start the development servers for both the client and server applications.
|
- In each terminal, run the command `pnpm dev` to start the development servers for both the client and server
|
||||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect`.
|
applications.
|
||||||
|
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by
|
||||||
|
running `pnpm dev:inspect`.
|
||||||
|
|
||||||
## TESTING
|
## TESTING
|
||||||
|
|
||||||
Generally we have 2 types of tests.
|
Generally we have 2 types of tests.
|
||||||
|
|
||||||
- Unit tests for functions that contain business logic
|
- Unit tests for functions that contain business logic
|
||||||
- End-to-end tests for core features
|
- End-to-end tests for core features
|
||||||
|
|
||||||
### Unit tests
|
### Unit tests
|
||||||
|
|
||||||
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
||||||
|
|
||||||
You can run unit tests by running turbo `turbo test:pipeline` from the project root.
|
You can run unit tests by running turbo `turbo test:pipeline` from the project root.
|
||||||
@@ -41,12 +51,20 @@ This will run all tests and close test runner.
|
|||||||
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
||||||
|
|
||||||
### E2E tests
|
### E2E tests
|
||||||
E2E tests are in a separate package. On running, [playwright](https://playwright.dev/) will spin up an instance of the webserver to test against
|
|
||||||
|
E2E tests are in a separate package. On running, [playwright](https://playwright.dev/) will spin up an instance of the
|
||||||
|
webserver to test against
|
||||||
These tests also run against a separate version of the DB (test-db)
|
These tests also run against a separate version of the DB (test-db)
|
||||||
|
|
||||||
You can run playwright tests from project root with `pnpm e2e`
|
You can run playwright tests from project root with `pnpm e2e`
|
||||||
|
|
||||||
When writing tests, it can be handy to run playwright in interactive mode with `pnpm e2e:i`. You would need to manually start the webserver with `pnpm dev:server`
|
When writing tests, it can be handy to run playwright in interactive mode with `pnpm e2e:i`. You would need to manually
|
||||||
|
start the webserver with `pnpm dev:server`
|
||||||
|
|
||||||
|
Some other useful commands
|
||||||
|
|
||||||
|
- `pnpm e2e --ui` open playwright UI
|
||||||
|
- `pnpm e2e --headed` run tests with a visible browser window
|
||||||
|
|
||||||
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
||||||
|
|
||||||
@@ -54,6 +72,7 @@ Ontime uses Electron to distribute the application.
|
|||||||
You can generate a distribution for your OS by running the following steps.
|
You can generate a distribution for your OS by running the following steps.
|
||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- __Build the UI and server__ by running `turbo build:local`
|
- __Build the UI and server__ by running `turbo build:local`
|
||||||
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
||||||
@@ -66,10 +85,12 @@ Ontime provides a docker-compose file to aid with building and running docker im
|
|||||||
While it should allow for a generic setup, it might need to be modified to fit your infrastructure.
|
While it should allow for a generic setup, it might need to be modified to fit your infrastructure.
|
||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- __Build docker image from__ by running `docker build -t getontime/ontime`
|
- __Build docker image from__ by running `docker build -t getontime/ontime`
|
||||||
- __Run docker image from compose__ by running `docker-compose up -d`
|
- __Run docker image from compose__ by running `docker-compose up -d`
|
||||||
|
|
||||||
Other useful commands
|
Other useful commands
|
||||||
|
|
||||||
- __List running processes__ by running `docker ps`
|
- __List running processes__ by running `docker ps`
|
||||||
- __Kill running process__ by running `docker kill <process-id>`
|
- __Kill running process__ by running `docker kill <process-id>`
|
||||||
|
|||||||
@@ -37,7 +37,17 @@ function ButtonTooltip(name: TimeEntryField, warning?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TimeInput(props: TimeInputProps) {
|
export default function TimeInput(props: TimeInputProps) {
|
||||||
const { id, name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
submitHandler,
|
||||||
|
time = 0,
|
||||||
|
delay = 0,
|
||||||
|
placeholder,
|
||||||
|
validationHandler,
|
||||||
|
previousEnd = 0,
|
||||||
|
warning,
|
||||||
|
} = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
@@ -191,7 +201,7 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
id={id}
|
id={id}
|
||||||
data-testid='time-input'
|
data-testid={`time-input-${name}`}
|
||||||
className={style.inputField}
|
className={style.inputField}
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import ScheduleItem from './ScheduleItem';
|
|||||||
import './Schedule.scss';
|
import './Schedule.scss';
|
||||||
|
|
||||||
interface ScheduleProps {
|
interface ScheduleProps {
|
||||||
|
isProduction?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Schedule({ className }: ScheduleProps) {
|
export default function Schedule({ isProduction, className }: ScheduleProps) {
|
||||||
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
||||||
|
|
||||||
if (paginatedEvents?.length < 1) {
|
if (paginatedEvents?.length < 1) {
|
||||||
@@ -30,12 +31,16 @@ export default function Schedule({ className }: ScheduleProps) {
|
|||||||
selectedState = 'future';
|
selectedState = 'future';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeStart = isProduction ? event.timeStart + (event?.delay ?? 0) : event.timeStart;
|
||||||
|
const timeEnd = isProduction ? event.timeEnd + (event?.delay ?? 0) : event.timeEnd;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScheduleItem
|
<ScheduleItem
|
||||||
key={event.id}
|
key={event.id}
|
||||||
selected={selectedState}
|
selected={selectedState}
|
||||||
timeStart={event.timeStart + (event?.delay ?? 0)}
|
timeStart={timeStart}
|
||||||
timeEnd={event.timeEnd + (event?.delay ?? 0)}
|
timeEnd={timeEnd}
|
||||||
title={event.title}
|
title={event.title}
|
||||||
colour={isBackstage ? event.colour : ''}
|
colour={isBackstage ? event.colour : ''}
|
||||||
backstageEvent={!event.isPublic}
|
backstageEvent={!event.isPublic}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
import { isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
import { getCueCandidate, swapOntimeEvents } from 'ontime-utils';
|
import { getCueCandidate, swapOntimeEvents } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
||||||
@@ -59,7 +59,7 @@ export const useEventAction = () => {
|
|||||||
const newEvent: Partial<OntimeRundownEntry> = { ...event };
|
const newEvent: Partial<OntimeRundownEntry> = { ...event };
|
||||||
|
|
||||||
// ************* CHECK OPTIONS specific to events
|
// ************* CHECK OPTIONS specific to events
|
||||||
if (newEvent.type === SupportedEvent.Event) {
|
if (isOntimeEvent(newEvent)) {
|
||||||
const applicationOptions = {
|
const applicationOptions = {
|
||||||
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
||||||
startTimeIsLastEnd: options?.startTimeIsLastEnd ?? startTimeIsLastEnd,
|
startTimeIsLastEnd: options?.startTimeIsLastEnd ?? startTimeIsLastEnd,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
||||||
import { millisToDelayString } from '../../common/utils/dateConfig';
|
import { millisToDelayString } from '../../common/utils/dateConfig';
|
||||||
@@ -196,15 +196,14 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((row) => {
|
{table.getRowModel().rows.map((row) => {
|
||||||
const entryType = row.original.type as SupportedEvent;
|
|
||||||
const key = row.original.id;
|
const key = row.original.id;
|
||||||
const isSelected = selectedId === key;
|
const isSelected = selectedId === key;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
isPast = false;
|
isPast = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entryType === SupportedEvent.Block) {
|
if (isOntimeBlock(row.original)) {
|
||||||
const title = (row.original as OntimeBlock).title;
|
const title = row.original.title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={key} className={style.blockRow}>
|
<tr key={key} className={style.blockRow}>
|
||||||
@@ -212,8 +211,8 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (entryType === SupportedEvent.Delay) {
|
if (isOntimeDelay(row.original)) {
|
||||||
const delayVal = (row.original as OntimeDelay).duration;
|
const delayVal = row.original.duration;
|
||||||
|
|
||||||
if (!showDelayBlock || delayVal === 0) {
|
if (!showDelayBlock || delayVal === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -226,7 +225,7 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (entryType === SupportedEvent.Event) {
|
if (isOntimeEvent(row.original)) {
|
||||||
eventIndex++;
|
eventIndex++;
|
||||||
const isSelected = key === selectedId;
|
const isSelected = key === selectedId;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
@@ -238,9 +237,9 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bgFallback = 'transparent';
|
const bgFallback = 'transparent';
|
||||||
const bgColour = (row.original as OntimeEvent).colour || bgFallback;
|
const bgColour = row.original.colour || bgFallback;
|
||||||
const textColour = bgColour === bgFallback ? undefined : getAccessibleColour(bgColour);
|
const textColour = bgColour === bgFallback ? undefined : getAccessibleColour(bgColour);
|
||||||
const isSkipped = (row.original as OntimeEvent).skip;
|
const isSkipped = row.original.skip;
|
||||||
|
|
||||||
let rowBgColour: string | undefined;
|
let rowBgColour: string | undefined;
|
||||||
if (row.original.id === selectedId) {
|
if (row.original.id === selectedId) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
@@ -29,8 +29,8 @@ export default function EventEditor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event = data.find((event) => event.id === openId);
|
const event = data.find((event) => event.id === openId);
|
||||||
if (event && event.type === SupportedEvent.Event) {
|
if (event && isOntimeEvent(event)) {
|
||||||
setEvent(event as OntimeEvent);
|
setEvent(event);
|
||||||
}
|
}
|
||||||
}, [data, openId]);
|
}, [data, openId]);
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
|
|
||||||
<ScheduleProvider events={filteredEvents} selectedEventId={selectedId} isBackstage>
|
<ScheduleProvider events={filteredEvents} selectedEventId={selectedId} isBackstage>
|
||||||
<ScheduleNav className='schedule-nav-container' />
|
<ScheduleNav className='schedule-nav-container' />
|
||||||
<Schedule className='schedule-container' />
|
<Schedule isProduction className='schedule-container' />
|
||||||
</ScheduleProvider>
|
</ScheduleProvider>
|
||||||
|
|
||||||
<div className={showPublicMessage ? 'public-container' : 'public-container public-container--hidden'}>
|
<div className={showPublicMessage ? 'public-container' : 'public-container public-container--hidden'}>
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||||
|
import { _applyDelay } from '../delayUtils.js';
|
||||||
|
|
||||||
|
describe('_applyDelay() ', () => {
|
||||||
|
describe('in a rundown without the delay field, persisted rundown', () => {
|
||||||
|
it('applies delays', () => {
|
||||||
|
const delayId = '1';
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
{ id: delayId, type: SupportedEvent.Delay, duration: 10 } as OntimeDelay,
|
||||||
|
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{ id: '2', type: SupportedEvent.Event, timeStart: 10, timeEnd: 20, duration: 10, revision: 2 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, timeStart: 10, timeEnd: 20, duration: 10, revision: 2 } as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = _applyDelay(delayId, testRundown);
|
||||||
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
it('applies negative delays', () => {
|
||||||
|
const delayId = '1';
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
{ id: delayId, type: SupportedEvent.Delay, duration: -10 } as OntimeDelay,
|
||||||
|
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, timeStart: 20, timeEnd: 40, duration: 20, revision: 1 } as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 2 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, timeStart: 10, timeEnd: 30, duration: 20, revision: 2 } as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = _applyDelay(delayId, testRundown);
|
||||||
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
it('maintains constant duration', () => {
|
||||||
|
const delayId = '1';
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
{ id: delayId, type: SupportedEvent.Delay, duration: -30 } as OntimeDelay,
|
||||||
|
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, timeStart: 20, timeEnd: 40, duration: 20, revision: 1 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{ id: '2', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 2 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, timeStart: 0, timeEnd: 20, duration: 20, revision: 2 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = _applyDelay(delayId, testRundown);
|
||||||
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('in a rundown with the delay field, cached rundown', () => {
|
||||||
|
it('applies delays', () => {
|
||||||
|
const delayId = '1';
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
{ id: delayId, type: SupportedEvent.Delay, duration: 10 } as OntimeDelay,
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: 10,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: 10,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 10,
|
||||||
|
timeEnd: 20,
|
||||||
|
duration: 10,
|
||||||
|
revision: 2,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 10,
|
||||||
|
timeEnd: 20,
|
||||||
|
duration: 10,
|
||||||
|
revision: 2,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = _applyDelay(delayId, testRundown);
|
||||||
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
it('applies negative delays', () => {
|
||||||
|
const delayId = '1';
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
{ id: delayId, type: SupportedEvent.Delay, duration: -10 } as OntimeDelay,
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: -10,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 20,
|
||||||
|
timeEnd: 40,
|
||||||
|
duration: 20,
|
||||||
|
revision: 1,
|
||||||
|
delay: -10,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 2,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 10,
|
||||||
|
timeEnd: 30,
|
||||||
|
duration: 20,
|
||||||
|
revision: 2,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = _applyDelay(delayId, testRundown);
|
||||||
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
it('maintains constant duration', () => {
|
||||||
|
const delayId = '1';
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
{ id: delayId, type: SupportedEvent.Delay, duration: -30 } as OntimeDelay,
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 1,
|
||||||
|
delay: -30,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 20,
|
||||||
|
timeEnd: 40,
|
||||||
|
duration: 20,
|
||||||
|
revision: 1,
|
||||||
|
delay: -30,
|
||||||
|
} as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 10,
|
||||||
|
duration: 10,
|
||||||
|
revision: 2,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
timeStart: 0,
|
||||||
|
timeEnd: 20,
|
||||||
|
duration: 20,
|
||||||
|
revision: 2,
|
||||||
|
delay: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedRundown = _applyDelay(delayId, testRundown);
|
||||||
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { isOntimeBlock, isOntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { deleteAtIndex } from '../utils/arrayUtils.js';
|
||||||
|
|
||||||
|
export function _applyDelay(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
||||||
|
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
||||||
|
const delayEvent = rundown.at(delayIndex);
|
||||||
|
|
||||||
|
if (delayEvent.type !== SupportedEvent.Delay) {
|
||||||
|
throw new Error('Given event ID is not a delay');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRundown = [...rundown];
|
||||||
|
const delayValue = delayEvent.duration;
|
||||||
|
|
||||||
|
if (delayValue === 0 || delayIndex === rundown.length - 1) {
|
||||||
|
// nothing to apply
|
||||||
|
return updatedRundown;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = delayIndex + 1; i < rundown.length; i++) {
|
||||||
|
const currentEvent = updatedRundown[i];
|
||||||
|
|
||||||
|
if (isOntimeBlock(currentEvent)) {
|
||||||
|
break;
|
||||||
|
} else if (isOntimeEvent(currentEvent)) {
|
||||||
|
currentEvent.timeStart = Math.max(0, currentEvent.timeStart + delayValue);
|
||||||
|
currentEvent.timeEnd = Math.max(currentEvent.duration, currentEvent.timeEnd + delayValue);
|
||||||
|
if (currentEvent.delay) {
|
||||||
|
currentEvent.delay = currentEvent.delay - delayValue;
|
||||||
|
}
|
||||||
|
currentEvent.revision += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteAtIndex(delayIndex, updatedRundown);
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
OntimeBlock,
|
OntimeBlock,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
|
||||||
Playback,
|
Playback,
|
||||||
SupportedEvent,
|
SupportedEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
@@ -18,6 +17,7 @@ import { sendRefetch } from '../../adapters/websocketAux.js';
|
|||||||
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||||
import {
|
import {
|
||||||
cachedAdd,
|
cachedAdd,
|
||||||
|
cachedApplyDelay,
|
||||||
cachedClear,
|
cachedClear,
|
||||||
cachedDelete,
|
cachedDelete,
|
||||||
cachedEdit,
|
cachedEdit,
|
||||||
@@ -166,7 +166,7 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
|||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
let insertIndex = 0;
|
let insertIndex = 0;
|
||||||
if (eventData?.after !== 'undefined') {
|
if (eventData?.after !== undefined) {
|
||||||
const index = DataProvider.getIndexOf(eventData.after);
|
const index = DataProvider.getIndexOf(eventData.after);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.after}`);
|
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.after}`);
|
||||||
@@ -268,46 +268,14 @@ export async function reorderEvent(eventId: string, from: number, to: number) {
|
|||||||
return reorderedItem;
|
return reorderedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _applyDelay(
|
export async function applyDelay(eventId: string) {
|
||||||
eventId: string,
|
await cachedApplyDelay(eventId);
|
||||||
rundown: OntimeRundown,
|
|
||||||
): {
|
|
||||||
delayIndex: number | null;
|
|
||||||
updatedRundown: OntimeRundown;
|
|
||||||
} {
|
|
||||||
const updatedRundown = [...rundown];
|
|
||||||
let delayIndex = null;
|
|
||||||
let delayValue = 0;
|
|
||||||
|
|
||||||
for (const [index, event] of updatedRundown.entries()) {
|
// notify timer service of changed events
|
||||||
// look for delay
|
updateTimer();
|
||||||
if (delayIndex === null) {
|
|
||||||
if (event.type === SupportedEvent.Delay && event.id === eventId) {
|
|
||||||
delayValue = event.duration;
|
|
||||||
delayIndex = index;
|
|
||||||
|
|
||||||
if (delayValue === 0) {
|
// advice socket subscribers of change
|
||||||
// nothing to apply
|
sendRefetch();
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// once delay is found, apply delay value to all items until block or end
|
|
||||||
if (event.type === SupportedEvent.Event) {
|
|
||||||
updatedRundown[index] = {
|
|
||||||
...event,
|
|
||||||
timeStart: Math.max(0, event.timeStart + delayValue),
|
|
||||||
timeEnd: Math.max(event.duration, event.timeEnd + delayValue),
|
|
||||||
revision: event.revision + 1,
|
|
||||||
};
|
|
||||||
} else if (event.type === SupportedEvent.Block) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { delayIndex, updatedRundown };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,22 +294,6 @@ export async function swapEvents(from: string, to: string) {
|
|||||||
sendRefetch();
|
sendRefetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* applies delay value for given event
|
|
||||||
* @param eventId
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export async function applyDelay(eventId: string) {
|
|
||||||
const rundown: OntimeRundown = DataProvider.getRundown();
|
|
||||||
const { delayIndex, updatedRundown } = _applyDelay(eventId, rundown);
|
|
||||||
if (delayIndex === null) {
|
|
||||||
throw new Error(`Delay event with ID ${eventId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await DataProvider.setRundown(updatedRundown);
|
|
||||||
await deleteEvent(eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forces update in the store
|
* Forces update in the store
|
||||||
* Called when we make changes to the rundown object
|
* Called when we make changes to the rundown object
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
import { EndAction, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
|
||||||
import { _applyDelay } from '../RundownService.js';
|
|
||||||
|
|
||||||
describe('applyDelay()', () => {
|
|
||||||
it('applies its duration to following events', () => {
|
|
||||||
const rundown: OntimeRundown = [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 600000,
|
|
||||||
timeEnd: 1200000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 4,
|
|
||||||
id: '659e1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 600000,
|
|
||||||
type: SupportedEvent.Delay,
|
|
||||||
revision: 0,
|
|
||||||
id: '07986',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 600000,
|
|
||||||
timeEnd: 1200000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 4,
|
|
||||||
id: 'd48c2',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const eventId = rundown[1].id;
|
|
||||||
const { delayIndex, updatedRundown } = _applyDelay(eventId, rundown);
|
|
||||||
|
|
||||||
expect(delayIndex).toBe(1);
|
|
||||||
// we do not delay delays anymore
|
|
||||||
expect(updatedRundown.length).toBe(3);
|
|
||||||
expect(rundown.length).toBe(3);
|
|
||||||
expect(updatedRundown[0].timeStart).toBe(rundown[0].timeStart);
|
|
||||||
expect(updatedRundown[2].timeStart).toBe(rundown[1].duration + rundown[2].timeStart);
|
|
||||||
expect(updatedRundown[2].timeEnd).toBe(rundown[1].duration + rundown[2].timeEnd);
|
|
||||||
});
|
|
||||||
it('stops propagating on blocks', () => {
|
|
||||||
const rundown: OntimeRundown = [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 600000,
|
|
||||||
timeEnd: 1200000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 0,
|
|
||||||
id: '659e1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 600000,
|
|
||||||
type: SupportedEvent.Delay,
|
|
||||||
revision: 0,
|
|
||||||
id: '07986',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 600000,
|
|
||||||
timeEnd: 1200000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 0,
|
|
||||||
id: 'd48c2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
type: SupportedEvent.Block,
|
|
||||||
id: '9870d',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 1200000,
|
|
||||||
timeEnd: 1800000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 2,
|
|
||||||
id: '2f185',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const eventId = rundown[1].id;
|
|
||||||
const { updatedRundown } = _applyDelay(eventId, rundown);
|
|
||||||
|
|
||||||
expect(updatedRundown[0].timeStart).toBe(rundown[0].timeStart);
|
|
||||||
expect(updatedRundown[2].timeStart).toBe(rundown[1].duration + rundown[2].timeStart);
|
|
||||||
expect(updatedRundown[4].timeStart).toBe(rundown[4].timeStart);
|
|
||||||
});
|
|
||||||
it('only applies given delay', () => {
|
|
||||||
const rundown: OntimeRundown = [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 600000,
|
|
||||||
timeEnd: 1200000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 0,
|
|
||||||
id: '659e1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 600000,
|
|
||||||
type: SupportedEvent.Delay,
|
|
||||||
revision: 0,
|
|
||||||
id: '07986',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 1200000,
|
|
||||||
timeEnd: 1200000,
|
|
||||||
duration: 0,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 0,
|
|
||||||
id: '1c48f',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 1200000,
|
|
||||||
type: SupportedEvent.Delay,
|
|
||||||
revision: 0,
|
|
||||||
id: '7db42',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 600000,
|
|
||||||
timeEnd: 1200000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 0,
|
|
||||||
id: 'd48c2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
type: SupportedEvent.Block,
|
|
||||||
id: '9870d',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
endAction: EndAction.None,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStart: 1200000,
|
|
||||||
timeEnd: 1800000,
|
|
||||||
duration: 600000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
|
||||||
revision: 0,
|
|
||||||
id: '2f185',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const eventId = rundown[1].id;
|
|
||||||
const { updatedRundown } = _applyDelay(eventId, rundown);
|
|
||||||
|
|
||||||
expect(updatedRundown[0].timeStart).toBe(rundown[0].timeStart);
|
|
||||||
expect(updatedRundown[2].timeStart).toBe(rundown[1].duration + rundown[2].timeStart);
|
|
||||||
expect(updatedRundown[4].timeStart).toBe(rundown[1].duration + rundown[4].timeStart);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
import {
|
||||||
|
isOntimeBlock,
|
||||||
|
isOntimeDelay,
|
||||||
|
isOntimeEvent,
|
||||||
|
OntimeBlock,
|
||||||
|
OntimeDelay,
|
||||||
|
OntimeEvent,
|
||||||
|
OntimeRundown,
|
||||||
|
OntimeRundownEntry,
|
||||||
|
} from 'ontime-types';
|
||||||
|
import { swapOntimeEvents } from 'ontime-utils';
|
||||||
|
|
||||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
|
import { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||||
import { isProduction } from '../../setup.js';
|
import { isProduction } from '../../setup.js';
|
||||||
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
|
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
|
||||||
import { swapOntimeEvents } from 'ontime-utils';
|
import { _applyDelay } from '../delayUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key of rundown in cache
|
* Key of rundown in cache
|
||||||
@@ -51,7 +61,7 @@ export async function cachedAdd(eventIndex: number, event: OntimeEvent | OntimeD
|
|||||||
let newDelayedRundown = insertAtIndex(eventIndex, event, delayedRundown);
|
let newDelayedRundown = insertAtIndex(eventIndex, event, delayedRundown);
|
||||||
|
|
||||||
// update delay cache
|
// update delay cache
|
||||||
if (event.type === SupportedEvent.Event) {
|
if (isOntimeEvent(event)) {
|
||||||
// if it is an event, we need its delay
|
// if it is an event, we need its delay
|
||||||
(newDelayedRundown[eventIndex] as OntimeEvent).delay = getDelayAt(eventIndex, newDelayedRundown);
|
(newDelayedRundown[eventIndex] as OntimeEvent).delay = getDelayAt(eventIndex, newDelayedRundown);
|
||||||
} else {
|
} else {
|
||||||
@@ -79,22 +89,20 @@ export async function cachedEdit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedRundown = DataProvider.getRundown();
|
const updatedRundown = DataProvider.getRundown();
|
||||||
const newEvent = { ...updatedRundown[indexInMemory], ...patchObject };
|
const newEvent = { ...updatedRundown[indexInMemory], ...patchObject } as OntimeRundownEntry;
|
||||||
if (newEvent.type === SupportedEvent.Event) {
|
if (isOntimeEvent(newEvent)) {
|
||||||
newEvent.revision++;
|
newEvent.revision++;
|
||||||
}
|
}
|
||||||
// @ts-expect-error -- this merge is safe
|
|
||||||
updatedRundown[indexInMemory] = newEvent;
|
updatedRundown[indexInMemory] = newEvent;
|
||||||
|
|
||||||
let newDelayedRundown = getDelayedRundown();
|
let newDelayedRundown = getDelayedRundown();
|
||||||
if (newDelayedRundown?.[indexInMemory].id !== newEvent.id) {
|
if (newDelayedRundown?.[indexInMemory].id !== newEvent.id) {
|
||||||
invalidateFromError();
|
invalidateFromError();
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error -- this merge is safe
|
|
||||||
newDelayedRundown[indexInMemory] = newEvent;
|
newDelayedRundown[indexInMemory] = newEvent;
|
||||||
if (newEvent.type === SupportedEvent.Event) {
|
if (isOntimeEvent(newEvent)) {
|
||||||
(newDelayedRundown[indexInMemory] as OntimeEvent).delay = getDelayAt(indexInMemory, newDelayedRundown);
|
(newDelayedRundown[indexInMemory] as OntimeEvent).delay = getDelayAt(indexInMemory, newDelayedRundown);
|
||||||
} else if (newEvent.type === SupportedEvent.Delay) {
|
} else if (isOntimeDelay(newEvent)) {
|
||||||
// blocks have no reason to change the rundown, from delays we need to recalculate
|
// blocks have no reason to change the rundown, from delays we need to recalculate
|
||||||
newDelayedRundown = calculateRuntimeDelaysFromIndex(indexInMemory, newDelayedRundown);
|
newDelayedRundown = calculateRuntimeDelaysFromIndex(indexInMemory, newDelayedRundown);
|
||||||
}
|
}
|
||||||
@@ -124,13 +132,12 @@ export async function cachedDelete(eventId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let updatedRundown = DataProvider.getRundown();
|
let updatedRundown = DataProvider.getRundown();
|
||||||
const eventType = updatedRundown[eventIndex].type;
|
|
||||||
updatedRundown = deleteAtIndex(eventIndex, updatedRundown);
|
updatedRundown = deleteAtIndex(eventIndex, updatedRundown);
|
||||||
if (eventId !== delayedRundown[eventIndex].id) {
|
if (eventId !== delayedRundown[eventIndex].id) {
|
||||||
invalidateFromError();
|
invalidateFromError();
|
||||||
} else {
|
} else {
|
||||||
delayedRundown = deleteAtIndex(eventIndex, delayedRundown);
|
delayedRundown = deleteAtIndex(eventIndex, delayedRundown);
|
||||||
if (eventType === SupportedEvent.Delay || eventType === SupportedEvent.Block) {
|
if (isOntimeDelay(updatedRundown[eventIndex]) || isOntimeBlock(updatedRundown[eventIndex])) {
|
||||||
// for events, we do not have to worry
|
// for events, we do not have to worry
|
||||||
// the following event, would have taken the place of the deleted event by now
|
// the following event, would have taken the place of the deleted event by now
|
||||||
delayedRundown = calculateRuntimeDelaysFromIndex(eventIndex, delayedRundown);
|
delayedRundown = calculateRuntimeDelaysFromIndex(eventIndex, delayedRundown);
|
||||||
@@ -205,6 +212,19 @@ export async function cachedSwap(fromEventId: string, toEventId: string) {
|
|||||||
await DataProvider.setRundown(rundownToUpdate);
|
await DataProvider.setRundown(rundownToUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cachedApplyDelay(eventId: string) {
|
||||||
|
// update persisted rundown
|
||||||
|
const rundown: OntimeRundown = DataProvider.getRundown();
|
||||||
|
const persistedRundown = _applyDelay(eventId, rundown);
|
||||||
|
|
||||||
|
const delayedRundown = getDelayedRundown();
|
||||||
|
const cachedRundown = _applyDelay(eventId, delayedRundown);
|
||||||
|
|
||||||
|
// update
|
||||||
|
runtimeCacheStore.setCached(delayedRundownCacheKey, cachedRundown);
|
||||||
|
await DataProvider.setRundown(persistedRundown);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates all delays in a given rundown
|
* Calculates all delays in a given rundown
|
||||||
* @param rundown
|
* @param rundown
|
||||||
@@ -214,11 +234,11 @@ export function calculateRuntimeDelays(rundown: OntimeRundown) {
|
|||||||
const updatedRundown = [...rundown];
|
const updatedRundown = [...rundown];
|
||||||
|
|
||||||
for (const [index, event] of updatedRundown.entries()) {
|
for (const [index, event] of updatedRundown.entries()) {
|
||||||
if (event.type === SupportedEvent.Delay) {
|
if (isOntimeDelay(event)) {
|
||||||
accumulatedDelay += event.duration;
|
accumulatedDelay += event.duration;
|
||||||
} else if (event.type === SupportedEvent.Block) {
|
} else if (isOntimeBlock(event)) {
|
||||||
accumulatedDelay = 0;
|
accumulatedDelay = 0;
|
||||||
} else if (event.type === SupportedEvent.Event) {
|
} else if (isOntimeEvent(event)) {
|
||||||
updatedRundown[index] = {
|
updatedRundown[index] = {
|
||||||
...event,
|
...event,
|
||||||
delay: accumulatedDelay,
|
delay: accumulatedDelay,
|
||||||
@@ -243,15 +263,15 @@ export function calculateRuntimeDelaysFromIndex(eventIndex: number, rundown: Ont
|
|||||||
|
|
||||||
for (let i = eventIndex; i < rundown.length; i++) {
|
for (let i = eventIndex; i < rundown.length; i++) {
|
||||||
const event = rundown[i];
|
const event = rundown[i];
|
||||||
if (event.type === SupportedEvent.Delay) {
|
if (isOntimeDelay(event)) {
|
||||||
accumulatedDelay += event.duration;
|
accumulatedDelay += event.duration;
|
||||||
} else if (event.type === SupportedEvent.Block) {
|
} else if (isOntimeBlock(event)) {
|
||||||
if (i === eventIndex) {
|
if (i === eventIndex) {
|
||||||
accumulatedDelay = 0;
|
accumulatedDelay = 0;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (event.type === SupportedEvent.Event) {
|
} else if (isOntimeEvent(event)) {
|
||||||
updatedRundown[i] = {
|
updatedRundown[i] = {
|
||||||
...event,
|
...event,
|
||||||
delay: accumulatedDelay,
|
delay: accumulatedDelay,
|
||||||
@@ -284,11 +304,11 @@ export function getDelayAt(eventIndex: number, rundown: OntimeRundown): number {
|
|||||||
// we need to check the event before
|
// we need to check the event before
|
||||||
const event = rundown[eventIndex - 1];
|
const event = rundown[eventIndex - 1];
|
||||||
|
|
||||||
if (event.type === SupportedEvent.Delay) {
|
if (isOntimeDelay(event)) {
|
||||||
return event.duration + getDelayAt(eventIndex - 1, rundown);
|
return event.duration + getDelayAt(eventIndex - 1, rundown);
|
||||||
} else if (event.type === SupportedEvent.Block) {
|
} else if (isOntimeBlock(event)) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (event.type === SupportedEvent.Event) {
|
} else if (isOntimeEvent(event)) {
|
||||||
return event.delay ?? 0;
|
return event.delay ?? 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test('delay blocks add time to events', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:4001/editor');
|
||||||
|
|
||||||
|
// delete all events and add a new one
|
||||||
|
await page.getByRole('button', { name: 'Event...' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete all events' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Event...' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Add event at start' }).click();
|
||||||
|
|
||||||
|
// add data to new event
|
||||||
|
await page.getByTestId('panel-rundown').getByPlaceholder('Start').click();
|
||||||
|
await page.getByTestId('panel-rundown').getByPlaceholder('Start').fill('10m');
|
||||||
|
await page.getByTestId('panel-rundown').getByPlaceholder('Start').press('Enter');
|
||||||
|
await page.getByTestId('panel-rundown').getByPlaceholder('End').click();
|
||||||
|
await page.getByTestId('panel-rundown').getByPlaceholder('End').fill('20m');
|
||||||
|
await page.getByTestId('panel-rundown').getByPlaceholder('End').press('Enter');
|
||||||
|
|
||||||
|
// add delay block
|
||||||
|
await page.getByRole('button', { name: 'Event...' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Add delay at start' }).click();
|
||||||
|
|
||||||
|
// fill positive delay
|
||||||
|
await page.getByTestId('delay-input').click();
|
||||||
|
await page.getByTestId('delay-input').fill('2m');
|
||||||
|
await page.getByTestId('delay-input').press('Enter');
|
||||||
|
await page.getByText('+2 minNew start: 00:12:00').click();
|
||||||
|
|
||||||
|
// make negative delay
|
||||||
|
await page.getByText('Subtract time').click();
|
||||||
|
await page.getByText('-2 minNew start: 00:08:00').click();
|
||||||
|
|
||||||
|
// apply delay
|
||||||
|
await page.getByRole('button', { name: 'Apply' }).click();
|
||||||
|
await expect(page.getByTestId('panel-rundown').getByTestId('time-input-timeStart')).toHaveValue('00:08:00');
|
||||||
|
|
||||||
|
// add new delay
|
||||||
|
await page.getByTestId('panel-rundown').getByPlaceholder('Start').click();
|
||||||
|
await page.getByRole('button', { name: 'Event...' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Add delay at start' }).click();
|
||||||
|
await page.getByTestId('delay-input').click();
|
||||||
|
await page.getByTestId('delay-input').fill('10m');
|
||||||
|
await page.getByTestId('delay-input').press('Enter');
|
||||||
|
await page.getByText('+10 minNew start: 00:18:00').click();
|
||||||
|
|
||||||
|
// cancel delay
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
await expect(page.getByTestId('panel-rundown').getByTestId('time-input-timeStart')).toHaveValue('00:08:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delays are show correctly', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:4001/editor');
|
||||||
|
|
||||||
|
// add a test event
|
||||||
|
await page.getByRole('button', { name: 'Event...' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete all events' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Create Event' }).click();
|
||||||
|
await page.getByTestId('time-input-timeStart').click();
|
||||||
|
await page.getByTestId('panel-rundown').getByTestId('time-input-timeStart').click();
|
||||||
|
await page.getByTestId('panel-rundown').getByTestId('time-input-timeStart').fill('10');
|
||||||
|
await page.getByTestId('panel-rundown').getByTestId('time-input-timeStart').press('Enter');
|
||||||
|
await page.getByTestId('panel-rundown').getByTestId('time-input-timeEnd').click();
|
||||||
|
await page.getByTestId('panel-rundown').getByTestId('time-input-timeEnd').fill('20');
|
||||||
|
await page.getByTestId('panel-rundown').getByTestId('time-input-timeEnd').press('Enter');
|
||||||
|
await page.getByText('Event title').click();
|
||||||
|
await page.getByPlaceholder('Event title').fill('test');
|
||||||
|
await page.getByPlaceholder('Event title').press('Enter');
|
||||||
|
|
||||||
|
await page.getByText('SED').click({ button: 'right' });
|
||||||
|
await page.getByRole('menuitem', { name: 'Toggle public' }).click();
|
||||||
|
|
||||||
|
// add a delay
|
||||||
|
await page.getByRole('button', { name: 'Event...' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Add delay at start' }).click();
|
||||||
|
await page.getByTestId('delay-input').click();
|
||||||
|
await page.getByTestId('delay-input').fill('1');
|
||||||
|
await page.getByTestId('delay-input').press('Enter');
|
||||||
|
|
||||||
|
// delay is shown in the editor
|
||||||
|
await page.getByText('+1 minNew start: 00:11:00').click();
|
||||||
|
|
||||||
|
// delay is shown in the cuesheet
|
||||||
|
await page.goto('http://localhost:4001/cuesheet');
|
||||||
|
await page.getByRole('cell', { name: '+1 min' }).click();
|
||||||
|
|
||||||
|
// delay is NOT shown in the public view
|
||||||
|
await page.goto('http://localhost:4001/public');
|
||||||
|
await page.getByText('00:10 → 00:20').click();
|
||||||
|
|
||||||
|
// delay is shown in the backstage view
|
||||||
|
await page.goto('http://localhost:4001/backstage');
|
||||||
|
await page.getByText('00:11 → 00:21').click();
|
||||||
|
});
|
||||||
@@ -46,5 +46,6 @@ export type { TitleBlock } from './definitions/runtime/TitleBlock.type.js';
|
|||||||
|
|
||||||
// CLIENT
|
// CLIENT
|
||||||
|
|
||||||
// UTILITY TYPES
|
// TYPE UTILITIES
|
||||||
|
export { isOntimeBlock, isOntimeDelay, isOntimeEvent } from './utils/guards.js';
|
||||||
export type { MaybeNumber } from './utils/utils.type.js';
|
export type { MaybeNumber } from './utils/utils.type.js';
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { OntimeRundownEntry } from '../definitions/core/Rundown.type.js';
|
||||||
|
import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from '../definitions/core/OntimeEvent.type.js';
|
||||||
|
|
||||||
|
export function isOntimeEvent(event: OntimeRundownEntry | Partial<OntimeRundownEntry>): event is OntimeEvent {
|
||||||
|
return event.type === SupportedEvent.Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOntimeDelay(event: OntimeRundownEntry | Partial<OntimeRundownEntry>): event is OntimeDelay {
|
||||||
|
return event.type === SupportedEvent.Delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOntimeBlock(event: OntimeRundownEntry | Partial<OntimeRundownEntry>): event is OntimeBlock {
|
||||||
|
return event.type === SupportedEvent.Block;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OntimeEvent, OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { dayInMs } from '../timeConstants.js';
|
import { dayInMs } from '../timeConstants.js';
|
||||||
|
|
||||||
@@ -18,8 +18,9 @@ export function getFirst(rundown: OntimeRundownEntry[]) {
|
|||||||
*/
|
*/
|
||||||
export function getFirstEvent(rundown: OntimeRundownEntry[]) {
|
export function getFirstEvent(rundown: OntimeRundownEntry[]) {
|
||||||
for (let i = 0; i < rundown.length; i++) {
|
for (let i = 0; i < rundown.length; i++) {
|
||||||
if (rundown[i].type === SupportedEvent.Event) {
|
const event = rundown[i];
|
||||||
return rundown[i] as OntimeEvent;
|
if (isOntimeEvent(event)) {
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -53,8 +54,9 @@ export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string):
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = index + 1; i < rundown.length; i++) {
|
for (let i = index + 1; i < rundown.length; i++) {
|
||||||
if (rundown[i].type === SupportedEvent.Event) {
|
const event = rundown[i];
|
||||||
return rundown[i] as OntimeEvent;
|
if (isOntimeEvent(event)) {
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -88,8 +90,9 @@ export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = index - 1; i >= 0; i--) {
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
if (rundown[i].type === SupportedEvent.Event) {
|
const event = rundown[i];
|
||||||
return rundown[i] as OntimeEvent;
|
if (isOntimeEvent(event)) {
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user