mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +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:
+29
-8
@@ -1,38 +1,48 @@
|
||||
# GETTING STARTED
|
||||
|
||||
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
|
||||
- __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
|
||||
|
||||
- __node__ (>=16.16)
|
||||
- __pnpm__ (>=7)
|
||||
- __docker__ (only necessary to run and build docker images)
|
||||
|
||||
## 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
|
||||
|
||||
From the project root, run the following commands
|
||||
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __Run dev mode__ by running `turbo dev`
|
||||
|
||||
### Debugging backend
|
||||
|
||||
To debug backend code in Node.js:
|
||||
|
||||
- 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.
|
||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect`.
|
||||
- In each terminal, run the command `pnpm dev` to start the development servers for both the client and server
|
||||
applications.
|
||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by
|
||||
running `pnpm dev:inspect`.
|
||||
|
||||
## TESTING
|
||||
|
||||
Generally we have 2 types of tests.
|
||||
Generally we have 2 types of tests.
|
||||
|
||||
- Unit tests for functions that contain business logic
|
||||
- End-to-end tests for core features
|
||||
|
||||
### Unit tests
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
|
||||
### 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)
|
||||
|
||||
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)
|
||||
|
||||
@@ -54,6 +72,7 @@ Ontime uses Electron to distribute the application.
|
||||
You can generate a distribution for your OS by running the following steps.
|
||||
|
||||
From the project root, run the following commands
|
||||
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __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`
|
||||
@@ -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.
|
||||
|
||||
From the project root, run the following commands
|
||||
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __Build docker image from__ by running `docker build -t getontime/ontime`
|
||||
- __Run docker image from compose__ by running `docker-compose up -d`
|
||||
|
||||
Other useful commands
|
||||
|
||||
- __List running processes__ by running `docker ps`
|
||||
- __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) {
|
||||
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 inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [value, setValue] = useState<string>('');
|
||||
@@ -191,7 +201,7 @@ export default function TimeInput(props: TimeInputProps) {
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
data-testid='time-input'
|
||||
data-testid={`time-input-${name}`}
|
||||
className={style.inputField}
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -6,10 +6,11 @@ import ScheduleItem from './ScheduleItem';
|
||||
import './Schedule.scss';
|
||||
|
||||
interface ScheduleProps {
|
||||
isProduction?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Schedule({ className }: ScheduleProps) {
|
||||
export default function Schedule({ isProduction, className }: ScheduleProps) {
|
||||
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
||||
|
||||
if (paginatedEvents?.length < 1) {
|
||||
@@ -30,12 +31,16 @@ export default function Schedule({ className }: ScheduleProps) {
|
||||
selectedState = 'future';
|
||||
}
|
||||
}
|
||||
|
||||
const timeStart = isProduction ? event.timeStart + (event?.delay ?? 0) : event.timeStart;
|
||||
const timeEnd = isProduction ? event.timeEnd + (event?.delay ?? 0) : event.timeEnd;
|
||||
|
||||
return (
|
||||
<ScheduleItem
|
||||
key={event.id}
|
||||
selected={selectedState}
|
||||
timeStart={event.timeStart + (event?.delay ?? 0)}
|
||||
timeEnd={event.timeEnd + (event?.delay ?? 0)}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
title={event.title}
|
||||
colour={isBackstage ? event.colour : ''}
|
||||
backstageEvent={!event.isPublic}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
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 { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
||||
@@ -59,7 +59,7 @@ export const useEventAction = () => {
|
||||
const newEvent: Partial<OntimeRundownEntry> = { ...event };
|
||||
|
||||
// ************* CHECK OPTIONS specific to events
|
||||
if (newEvent.type === SupportedEvent.Event) {
|
||||
if (isOntimeEvent(newEvent)) {
|
||||
const applicationOptions = {
|
||||
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
||||
startTimeIsLastEnd: options?.startTimeIsLastEnd ?? startTimeIsLastEnd,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@dnd-kit/core';
|
||||
import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
||||
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 { millisToDelayString } from '../../common/utils/dateConfig';
|
||||
@@ -196,15 +196,14 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
||||
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
const entryType = row.original.type as SupportedEvent;
|
||||
const key = row.original.id;
|
||||
const isSelected = selectedId === key;
|
||||
if (isSelected) {
|
||||
isPast = false;
|
||||
}
|
||||
|
||||
if (entryType === SupportedEvent.Block) {
|
||||
const title = (row.original as OntimeBlock).title;
|
||||
if (isOntimeBlock(row.original)) {
|
||||
const title = row.original.title;
|
||||
|
||||
return (
|
||||
<tr key={key} className={style.blockRow}>
|
||||
@@ -212,8 +211,8 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
if (entryType === SupportedEvent.Delay) {
|
||||
const delayVal = (row.original as OntimeDelay).duration;
|
||||
if (isOntimeDelay(row.original)) {
|
||||
const delayVal = row.original.duration;
|
||||
|
||||
if (!showDelayBlock || delayVal === 0) {
|
||||
return null;
|
||||
@@ -226,7 +225,7 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
if (entryType === SupportedEvent.Event) {
|
||||
if (isOntimeEvent(row.original)) {
|
||||
eventIndex++;
|
||||
const isSelected = key === selectedId;
|
||||
if (isSelected) {
|
||||
@@ -238,9 +237,9 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
||||
}
|
||||
|
||||
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 isSkipped = (row.original as OntimeEvent).skip;
|
||||
const isSkipped = row.original.skip;
|
||||
|
||||
let rowBgColour: string | undefined;
|
||||
if (row.original.id === selectedId) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useEventAction } from '../../common/hooks/useEventAction';
|
||||
@@ -29,8 +29,8 @@ export default function EventEditor() {
|
||||
}
|
||||
|
||||
const event = data.find((event) => event.id === openId);
|
||||
if (event && event.type === SupportedEvent.Event) {
|
||||
setEvent(event as OntimeEvent);
|
||||
if (event && isOntimeEvent(event)) {
|
||||
setEvent(event);
|
||||
}
|
||||
}, [data, openId]);
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ export default function Backstage(props: BackstageProps) {
|
||||
|
||||
<ScheduleProvider events={filteredEvents} selectedEventId={selectedId} isBackstage>
|
||||
<ScheduleNav className='schedule-nav-container' />
|
||||
<Schedule className='schedule-container' />
|
||||
<Schedule isProduction className='schedule-container' />
|
||||
</ScheduleProvider>
|
||||
|
||||
<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,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
Playback,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
@@ -18,6 +17,7 @@ import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||
import {
|
||||
cachedAdd,
|
||||
cachedApplyDelay,
|
||||
cachedClear,
|
||||
cachedDelete,
|
||||
cachedEdit,
|
||||
@@ -166,7 +166,7 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
||||
const id = generateId();
|
||||
|
||||
let insertIndex = 0;
|
||||
if (eventData?.after !== 'undefined') {
|
||||
if (eventData?.after !== undefined) {
|
||||
const index = DataProvider.getIndexOf(eventData.after);
|
||||
if (index < 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function _applyDelay(
|
||||
eventId: string,
|
||||
rundown: OntimeRundown,
|
||||
): {
|
||||
delayIndex: number | null;
|
||||
updatedRundown: OntimeRundown;
|
||||
} {
|
||||
const updatedRundown = [...rundown];
|
||||
let delayIndex = null;
|
||||
let delayValue = 0;
|
||||
export async function applyDelay(eventId: string) {
|
||||
await cachedApplyDelay(eventId);
|
||||
|
||||
for (const [index, event] of updatedRundown.entries()) {
|
||||
// look for delay
|
||||
if (delayIndex === null) {
|
||||
if (event.type === SupportedEvent.Delay && event.id === eventId) {
|
||||
delayValue = event.duration;
|
||||
delayIndex = index;
|
||||
// notify timer service of changed events
|
||||
updateTimer();
|
||||
|
||||
if (delayValue === 0) {
|
||||
// nothing to apply
|
||||
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 };
|
||||
// advice socket subscribers of change
|
||||
sendRefetch();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,22 +294,6 @@ export async function swapEvents(from: string, to: string) {
|
||||
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
|
||||
* 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 { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||
import { isProduction } from '../../setup.js';
|
||||
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
|
||||
import { swapOntimeEvents } from 'ontime-utils';
|
||||
import { _applyDelay } from '../delayUtils.js';
|
||||
|
||||
/**
|
||||
* Key of rundown in cache
|
||||
@@ -51,7 +61,7 @@ export async function cachedAdd(eventIndex: number, event: OntimeEvent | OntimeD
|
||||
let newDelayedRundown = insertAtIndex(eventIndex, event, delayedRundown);
|
||||
|
||||
// update delay cache
|
||||
if (event.type === SupportedEvent.Event) {
|
||||
if (isOntimeEvent(event)) {
|
||||
// if it is an event, we need its delay
|
||||
(newDelayedRundown[eventIndex] as OntimeEvent).delay = getDelayAt(eventIndex, newDelayedRundown);
|
||||
} else {
|
||||
@@ -79,22 +89,20 @@ export async function cachedEdit(
|
||||
}
|
||||
|
||||
const updatedRundown = DataProvider.getRundown();
|
||||
const newEvent = { ...updatedRundown[indexInMemory], ...patchObject };
|
||||
if (newEvent.type === SupportedEvent.Event) {
|
||||
const newEvent = { ...updatedRundown[indexInMemory], ...patchObject } as OntimeRundownEntry;
|
||||
if (isOntimeEvent(newEvent)) {
|
||||
newEvent.revision++;
|
||||
}
|
||||
// @ts-expect-error -- this merge is safe
|
||||
updatedRundown[indexInMemory] = newEvent;
|
||||
|
||||
let newDelayedRundown = getDelayedRundown();
|
||||
if (newDelayedRundown?.[indexInMemory].id !== newEvent.id) {
|
||||
invalidateFromError();
|
||||
} else {
|
||||
// @ts-expect-error -- this merge is safe
|
||||
newDelayedRundown[indexInMemory] = newEvent;
|
||||
if (newEvent.type === SupportedEvent.Event) {
|
||||
if (isOntimeEvent(newEvent)) {
|
||||
(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
|
||||
newDelayedRundown = calculateRuntimeDelaysFromIndex(indexInMemory, newDelayedRundown);
|
||||
}
|
||||
@@ -124,13 +132,12 @@ export async function cachedDelete(eventId: string) {
|
||||
}
|
||||
|
||||
let updatedRundown = DataProvider.getRundown();
|
||||
const eventType = updatedRundown[eventIndex].type;
|
||||
updatedRundown = deleteAtIndex(eventIndex, updatedRundown);
|
||||
if (eventId !== delayedRundown[eventIndex].id) {
|
||||
invalidateFromError();
|
||||
} else {
|
||||
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
|
||||
// the following event, would have taken the place of the deleted event by now
|
||||
delayedRundown = calculateRuntimeDelaysFromIndex(eventIndex, delayedRundown);
|
||||
@@ -205,6 +212,19 @@ export async function cachedSwap(fromEventId: string, toEventId: string) {
|
||||
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
|
||||
* @param rundown
|
||||
@@ -214,11 +234,11 @@ export function calculateRuntimeDelays(rundown: OntimeRundown) {
|
||||
const updatedRundown = [...rundown];
|
||||
|
||||
for (const [index, event] of updatedRundown.entries()) {
|
||||
if (event.type === SupportedEvent.Delay) {
|
||||
if (isOntimeDelay(event)) {
|
||||
accumulatedDelay += event.duration;
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
} else if (isOntimeBlock(event)) {
|
||||
accumulatedDelay = 0;
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
} else if (isOntimeEvent(event)) {
|
||||
updatedRundown[index] = {
|
||||
...event,
|
||||
delay: accumulatedDelay,
|
||||
@@ -243,15 +263,15 @@ export function calculateRuntimeDelaysFromIndex(eventIndex: number, rundown: Ont
|
||||
|
||||
for (let i = eventIndex; i < rundown.length; i++) {
|
||||
const event = rundown[i];
|
||||
if (event.type === SupportedEvent.Delay) {
|
||||
if (isOntimeDelay(event)) {
|
||||
accumulatedDelay += event.duration;
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
} else if (isOntimeBlock(event)) {
|
||||
if (i === eventIndex) {
|
||||
accumulatedDelay = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
} else if (isOntimeEvent(event)) {
|
||||
updatedRundown[i] = {
|
||||
...event,
|
||||
delay: accumulatedDelay,
|
||||
@@ -284,11 +304,11 @@ export function getDelayAt(eventIndex: number, rundown: OntimeRundown): number {
|
||||
// we need to check the event before
|
||||
const event = rundown[eventIndex - 1];
|
||||
|
||||
if (event.type === SupportedEvent.Delay) {
|
||||
if (isOntimeDelay(event)) {
|
||||
return event.duration + getDelayAt(eventIndex - 1, rundown);
|
||||
} else if (event.type === SupportedEvent.Block) {
|
||||
} else if (isOntimeBlock(event)) {
|
||||
return 0;
|
||||
} else if (event.type === SupportedEvent.Event) {
|
||||
} else if (isOntimeEvent(event)) {
|
||||
return event.delay ?? 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
|
||||
|
||||
// UTILITY TYPES
|
||||
// TYPE UTILITIES
|
||||
export { isOntimeBlock, isOntimeDelay, isOntimeEvent } from './utils/guards.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';
|
||||
|
||||
@@ -18,8 +18,9 @@ export function getFirst(rundown: OntimeRundownEntry[]) {
|
||||
*/
|
||||
export function getFirstEvent(rundown: OntimeRundownEntry[]) {
|
||||
for (let i = 0; i < rundown.length; i++) {
|
||||
if (rundown[i].type === SupportedEvent.Event) {
|
||||
return rundown[i] as OntimeEvent;
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -53,8 +54,9 @@ export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string):
|
||||
}
|
||||
|
||||
for (let i = index + 1; i < rundown.length; i++) {
|
||||
if (rundown[i].type === SupportedEvent.Event) {
|
||||
return rundown[i] as OntimeEvent;
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -88,8 +90,9 @@ export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: strin
|
||||
}
|
||||
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if (rundown[i].type === SupportedEvent.Event) {
|
||||
return rundown[i] as OntimeEvent;
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user