mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d427a6bbe | |||
| 23dcca5527 | |||
| a3bbd74db7 | |||
| 5323e627fb | |||
| cd9dbcdc68 | |||
| af99882919 | |||
| 1d4dc3a26f | |||
| e5fdf27f6c | |||
| 657cc22b44 | |||
| 51c31adaf1 |
+29
-8
@@ -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>`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "2.3.9",
|
"version": "2.7.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.7.0",
|
"@chakra-ui/react": "^2.7.0",
|
||||||
|
|||||||
+12
-12
@@ -4,9 +4,9 @@ import { ChakraProvider } from '@chakra-ui/react';
|
|||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
|
import { ContextMenu } from './common/components/context-menu/ContextMenu';
|
||||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
import { ContextMenuProvider } from './common/context/ContextMenuContext';
|
|
||||||
import useElectronEvent from './common/hooks/useElectronEvent';
|
import useElectronEvent from './common/hooks/useElectronEvent';
|
||||||
import { ontimeQueryClient } from './common/queryClient';
|
import { ontimeQueryClient } from './common/queryClient';
|
||||||
import { socketClientName } from './common/stores/connectionName';
|
import { socketClientName } from './common/stores/connectionName';
|
||||||
@@ -52,18 +52,18 @@ function App() {
|
|||||||
<ChakraProvider resetCSS theme={theme}>
|
<ChakraProvider resetCSS theme={theme}>
|
||||||
<QueryClientProvider client={ontimeQueryClient}>
|
<QueryClientProvider client={ontimeQueryClient}>
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
<ContextMenuProvider>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<div className='App'>
|
||||||
<div className='App'>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<TranslationProvider>
|
||||||
<TranslationProvider>
|
<ContextMenu>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</TranslationProvider>
|
</ContextMenu>
|
||||||
</ErrorBoundary>
|
</TranslationProvider>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
</ErrorBoundary>
|
||||||
</div>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</BrowserRouter>
|
</div>
|
||||||
</ContextMenuProvider>
|
</BrowserRouter>
|
||||||
</AppContextProvider>
|
</AppContextProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import withData from './features/viewers/ViewWrapper';
|
|
||||||
import withAlias from './features/AliasWrapper';
|
import withAlias from './features/AliasWrapper';
|
||||||
|
import withData from './features/viewers/ViewWrapper';
|
||||||
|
|
||||||
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
||||||
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
|
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export const STATIC_PORT = 4001;
|
|
||||||
|
|
||||||
// REST stuff
|
// REST stuff
|
||||||
export const EVENT_DATA = ['eventdata'];
|
export const EVENT_DATA = ['eventdata'];
|
||||||
export const ALIASES = ['aliases'];
|
export const ALIASES = ['aliases'];
|
||||||
@@ -14,9 +12,11 @@ export const RUNTIME = ['runtimeStore'];
|
|||||||
|
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
export const isProduction = import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
export const serverPort = import.meta.env.DEV ? STATIC_PORT : location.port;
|
const STATIC_PORT = 4001;
|
||||||
export const serverURL = import.meta.env.DEV ? `http://${location.hostname}:${serverPort}` : location.origin;
|
export const serverPort = isProduction ? location.port : STATIC_PORT;
|
||||||
|
export const serverURL = `${location.protocol}//${location.hostname}:${serverPort}`;
|
||||||
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
|
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
|
||||||
|
|
||||||
export const eventURL = `${serverURL}/eventdata`;
|
export const eventURL = `${serverURL}/eventdata`;
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import { addLog } from '../stores/logger';
|
|||||||
import { nowInMillis } from '../utils/time';
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
export function logAxiosError(prepend: string, error: unknown) {
|
export function logAxiosError(prepend: string, error: unknown) {
|
||||||
const message = axios.isAxiosError(error)
|
let message;
|
||||||
? `${prepend} ${(error as AxiosError).response?.statusText ?? ''}: ${(error as AxiosError).response?.data ?? ''}`
|
if (axios.isAxiosError(error)) {
|
||||||
: `${prepend}: ${error}`;
|
const statusText = (error as AxiosError).response?.statusText ?? '';
|
||||||
|
const data = (error as AxiosError).response?.data ?? '';
|
||||||
|
message = `${prepend} ${statusText}: ${data}`;
|
||||||
|
} else {
|
||||||
|
message = `${prepend}: ${error}`;
|
||||||
|
}
|
||||||
|
|
||||||
addLog({
|
addLog({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ export async function requestApplyDelay(eventId: string) {
|
|||||||
return axios.patch(`${rundownURL}/applydelay/${eventId}`);
|
return axios.patch(`${rundownURL}/applydelay/${eventId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SwapEntry = {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to swap two events
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function requestEventSwap(data: SwapEntry) {
|
||||||
|
return axios.patch(`${rundownURL}/swap`, data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to delete given event
|
* @description HTTP request to delete given event
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import { Alias, EventData, OSCSettings, OscSubscription, Settings, UserFields, ViewSettings } from 'ontime-types';
|
||||||
Alias,
|
|
||||||
EventData,
|
|
||||||
OSCSettings,
|
|
||||||
OscSubscription,
|
|
||||||
Settings,
|
|
||||||
UserFields,
|
|
||||||
ViewSettings,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
import { apiRepoLatest } from '../../externals';
|
||||||
import { InfoType } from '../models/Info';
|
import { InfoType } from '../models/Info';
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// logic (with some modifications) culled from:
|
||||||
|
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
||||||
|
|
||||||
|
import { Fragment, ReactElement } from 'react';
|
||||||
|
import { Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
|
||||||
|
import { IconType } from '@react-icons/all-files';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import style from './ContextMenu.module.scss';
|
||||||
|
|
||||||
|
type ContextMenuCoords = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Option = {
|
||||||
|
label: string;
|
||||||
|
icon: IconType;
|
||||||
|
onClick: () => void;
|
||||||
|
withDivider?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContextMenuStore = {
|
||||||
|
coords: ContextMenuCoords;
|
||||||
|
options: Option[];
|
||||||
|
isOpen: boolean;
|
||||||
|
setContextMenu: (coords: ContextMenuCoords, options: Option[]) => void;
|
||||||
|
setIsOpen: (newIsOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
|
||||||
|
coords: { x: 0, y: 0 },
|
||||||
|
options: [],
|
||||||
|
isOpen: false,
|
||||||
|
setContextMenu: (coords, options) => set(() => ({ coords, options, isOpen: true })),
|
||||||
|
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
// ReactElement type required due to early `return` (line 51) returning {children}
|
||||||
|
children: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextMenu = ({ children }: ContextMenuProps) => {
|
||||||
|
const { coords, options, isOpen, setIsOpen } = useContextMenuStore();
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
return setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<div className={style.contextMenuBackdrop} />
|
||||||
|
<Menu isOpen gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
||||||
|
<MenuButton
|
||||||
|
className={style.contextMenuButton}
|
||||||
|
aria-hidden
|
||||||
|
w={1}
|
||||||
|
h={1}
|
||||||
|
style={{
|
||||||
|
left: coords.x,
|
||||||
|
top: coords.y,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
{options.map(({ label, icon: Icon, onClick, withDivider, isDisabled }, i) => (
|
||||||
|
<Fragment key={label}>
|
||||||
|
{withDivider && <MenuDivider />}
|
||||||
|
<MenuItem key={i} icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { TitleActions } from '../../../../features/event-editor/composite/EventEditorTitles';
|
import { TitleActions } from '../../../../features/event-editor/composite/EventEditorDataLeft';
|
||||||
|
|
||||||
import Swatch from './Swatch';
|
import Swatch from './Swatch';
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
timeStart={timeStart}
|
||||||
timeEnd={event.timeEnd}
|
timeEnd={timeEnd}
|
||||||
title={event.title}
|
title={event.title}
|
||||||
colour={isBackstage ? event.colour : ''}
|
colour={isBackstage ? event.colour : ''}
|
||||||
backstageEvent={!event.isPublic}
|
backstageEvent={!event.isPublic}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
// logic (with some modifications) culled from:
|
|
||||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
|
||||||
|
|
||||||
import { createContext, ReactNode, useState } from 'react';
|
|
||||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
|
||||||
import { IconType } from '@react-icons/all-files';
|
|
||||||
|
|
||||||
import style from './ContextMenuContext.module.scss';
|
|
||||||
|
|
||||||
type ContextMenuCoords = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ContextMenuContextType = {
|
|
||||||
createContextMenu: (options: Option[], menuCoordinates: ContextMenuCoords) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContextMenuContext = createContext<ContextMenuContextType | null>(null);
|
|
||||||
|
|
||||||
export type Option = {
|
|
||||||
label: string;
|
|
||||||
icon: IconType;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ContextMenuProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const [coords, setCoords] = useState<ContextMenuCoords>({ x: 0, y: 0 });
|
|
||||||
const [options, setOptions] = useState<Option[]>([]);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
return setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createContextMenu = (options: Option[], menuCoords: ContextMenuCoords) => {
|
|
||||||
setCoords(menuCoords);
|
|
||||||
setOptions(options);
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ContextMenuContext.Provider value={{ createContextMenu }}>
|
|
||||||
{children}
|
|
||||||
{isOpen && (
|
|
||||||
<>
|
|
||||||
<div className={style.contextMenuBackdrop} />
|
|
||||||
<Menu isOpen gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
|
||||||
<MenuButton
|
|
||||||
className={style.contextMenuButton}
|
|
||||||
aria-hidden
|
|
||||||
w={1}
|
|
||||||
h={1}
|
|
||||||
style={{
|
|
||||||
left: coords.x,
|
|
||||||
top: coords.y,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuList>
|
|
||||||
{options.map(({ label, icon: Icon, onClick }, i) => (
|
|
||||||
<MenuItem key={i} icon={<Icon />} onClick={onClick}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ContextMenuContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,22 +1,16 @@
|
|||||||
import { MouseEvent, useContext } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { ContextMenuContext, Option } from '../context/ContextMenuContext';
|
import { Option, useContextMenuStore } from '../components/context-menu/ContextMenu';
|
||||||
|
|
||||||
export const useContextMenu = <T extends HTMLElement>(options: Option[]) => {
|
export const useContextMenu = <T extends HTMLElement>(options: Option[]) => {
|
||||||
const contextMenuContext = useContext(ContextMenuContext);
|
const { setContextMenu } = useContextMenuStore();
|
||||||
|
|
||||||
if (contextMenuContext === null) {
|
|
||||||
throw new Error('useContextMenu should be wrapped by ContextMenuProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { createContextMenu } = contextMenuContext;
|
|
||||||
|
|
||||||
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
|
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
|
||||||
// prevent browser default context menu from showing up
|
// prevent browser default context menu from showing up
|
||||||
contextMenuEvent.preventDefault();
|
contextMenuEvent.preventDefault();
|
||||||
|
|
||||||
const { pageX, pageY } = contextMenuEvent;
|
const { pageX, pageY } = contextMenuEvent;
|
||||||
return createContextMenu(options, { x: pageX, y: pageY });
|
return setContextMenu({ x: pageX, y: pageY }, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [localCreateContextMenu];
|
return [localCreateContextMenu];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
import { logAxiosError } from '../api/apiUtils';
|
||||||
@@ -9,9 +10,11 @@ import {
|
|||||||
requestApplyDelay,
|
requestApplyDelay,
|
||||||
requestDelete,
|
requestDelete,
|
||||||
requestDeleteAll,
|
requestDeleteAll,
|
||||||
|
requestEventSwap,
|
||||||
requestPostEvent,
|
requestPostEvent,
|
||||||
requestPutEvent,
|
requestPutEvent,
|
||||||
requestReorderEvent,
|
requestReorderEvent,
|
||||||
|
SwapEntry,
|
||||||
} from '../api/eventsApi';
|
} from '../api/eventsApi';
|
||||||
import { useEditorSettings } from '../stores/editorSettings';
|
import { useEditorSettings } from '../stores/editorSettings';
|
||||||
|
|
||||||
@@ -28,9 +31,10 @@ export const useEventAction = () => {
|
|||||||
* Calls mutation to add new event
|
* Calls mutation to add new event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _addEventMutation = useMutation(requestPostEvent, {
|
const _addEventMutation = useMutation({
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
|
mutationFn: requestPostEvent,
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
||||||
},
|
},
|
||||||
@@ -55,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,
|
||||||
@@ -63,16 +67,20 @@ export const useEventAction = () => {
|
|||||||
after: options?.after,
|
after: options?.after,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (newEvent?.cue === undefined) {
|
||||||
|
newEvent.cue = getCueCandidate(queryClient.getQueryData(RUNDOWN_TABLE) || [], options?.after);
|
||||||
|
}
|
||||||
|
|
||||||
// hard coding duration value to be as expected for now
|
// hard coding duration value to be as expected for now
|
||||||
// this until timeOptions gets implemented
|
// this until timeOptions gets implemented
|
||||||
if (typeof newEvent?.timeStart !== 'undefined' && typeof newEvent.timeEnd !== 'undefined') {
|
if (newEvent?.timeStart !== undefined && newEvent.timeEnd !== undefined) {
|
||||||
newEvent.duration = Math.max(0, newEvent?.timeEnd - newEvent?.timeStart) || 0;
|
newEvent.duration = Math.max(0, newEvent?.timeEnd - newEvent?.timeStart) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
||||||
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
||||||
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
||||||
if (typeof previousEvent !== 'undefined' && previousEvent.type === 'event') {
|
if (previousEvent !== undefined && previousEvent.type === 'event') {
|
||||||
newEvent.timeStart = previousEvent.timeEnd;
|
newEvent.timeStart = previousEvent.timeEnd;
|
||||||
newEvent.timeEnd = previousEvent.timeEnd;
|
newEvent.timeEnd = previousEvent.timeEnd;
|
||||||
}
|
}
|
||||||
@@ -102,7 +110,8 @@ export const useEventAction = () => {
|
|||||||
* Calls mutation to update existing event
|
* Calls mutation to update existing event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _updateEventMutation = useMutation(requestPutEvent, {
|
const _updateEventMutation = useMutation({
|
||||||
|
mutationFn: requestPutEvent,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (newEvent) => {
|
onMutate: async (newEvent) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
@@ -117,7 +126,6 @@ export const useEventAction = () => {
|
|||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvent, newEvent };
|
return { previousEvent, newEvent };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _newEvent, context) => {
|
onError: (_error, _newEvent, context) => {
|
||||||
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
|
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
|
||||||
@@ -148,7 +156,8 @@ export const useEventAction = () => {
|
|||||||
* Calls mutation to delete an event
|
* Calls mutation to delete an event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _deleteEventMutation = useMutation(requestDelete, {
|
const _deleteEventMutation = useMutation({
|
||||||
|
mutationFn: requestDelete,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (eventId) => {
|
onMutate: async (eventId) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
@@ -196,7 +205,8 @@ export const useEventAction = () => {
|
|||||||
* Calls mutation to delete all events
|
* Calls mutation to delete all events
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _deleteAllEventsMutation = useMutation(requestDeleteAll, {
|
const _deleteAllEventsMutation = useMutation({
|
||||||
|
mutationFn: requestDeleteAll,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async () => {
|
onMutate: async () => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
@@ -239,7 +249,8 @@ export const useEventAction = () => {
|
|||||||
* Calls mutation to apply a delay
|
* Calls mutation to apply a delay
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _applyDelayMutation = useMutation(requestApplyDelay, {
|
const _applyDelayMutation = useMutation({
|
||||||
|
mutationFn: requestApplyDelay,
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
||||||
@@ -265,7 +276,8 @@ export const useEventAction = () => {
|
|||||||
* Calls mutation to reorder an event
|
* Calls mutation to reorder an event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _reorderEventMutation = useMutation(requestReorderEvent, {
|
const _reorderEventMutation = useMutation({
|
||||||
|
mutationFn: requestReorderEvent,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (data) => {
|
onMutate: async (data) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
@@ -316,5 +328,67 @@ export const useEventAction = () => {
|
|||||||
[_reorderEventMutation],
|
[_reorderEventMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { addEvent, updateEvent, deleteEvent, deleteAllEvents, applyDelay, reorderEvent };
|
/**
|
||||||
|
* Calls mutation to swap events
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _swapEvents = useMutation({
|
||||||
|
mutationFn: requestEventSwap,
|
||||||
|
// we optimistically update here
|
||||||
|
onMutate: async ({ from, to }) => {
|
||||||
|
// cancel ongoing queries
|
||||||
|
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
|
||||||
|
|
||||||
|
// Snapshot the previous value
|
||||||
|
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
||||||
|
|
||||||
|
const fromEventIndex = rundown.findIndex((event) => event.id === from);
|
||||||
|
const toEventIndex = rundown.findIndex((event) => event.id === to);
|
||||||
|
|
||||||
|
const previousEvents = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
|
||||||
|
|
||||||
|
// optimistically update object
|
||||||
|
queryClient.setQueryData(RUNDOWN_TABLE, previousEvents);
|
||||||
|
|
||||||
|
// Return a context with the previous events
|
||||||
|
return { previousEvents };
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mutation fails, rollback undoes optimist update
|
||||||
|
onError: (_error, _eventId, context) => {
|
||||||
|
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
||||||
|
},
|
||||||
|
// Mutation finished, failed or successful
|
||||||
|
// Fetch anyway, just to be sure
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
||||||
|
},
|
||||||
|
networkMode: 'always',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps the schedule of two events
|
||||||
|
*/
|
||||||
|
const swapEvents = useCallback(
|
||||||
|
async ({ from, to }: SwapEntry) => {
|
||||||
|
// TODO: before calling `/swapEvents`,
|
||||||
|
// we should determine the events are of type `OntimeEvent`
|
||||||
|
try {
|
||||||
|
await _swapEvents.mutateAsync({ from, to });
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error re-ordering event', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[_swapEvents],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
addEvent,
|
||||||
|
updateEvent,
|
||||||
|
deleteEvent,
|
||||||
|
deleteAllEvents,
|
||||||
|
applyDelay,
|
||||||
|
reorderEvent,
|
||||||
|
swapEvents,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,536 +0,0 @@
|
|||||||
import { formatEventList, getEventsWithDelay, trimRundown } from '../eventsManager';
|
|
||||||
|
|
||||||
describe('getEventsWithDelay function', () => {
|
|
||||||
test('with positive delays', () => {
|
|
||||||
const testData = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 28800000,
|
|
||||||
timeEnd: 30600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 60000,
|
|
||||||
type: 'delay',
|
|
||||||
id: '24240',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
timeStart: 34920000,
|
|
||||||
timeEnd: 35520000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8ee5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Use simpler times to create a timer',
|
|
||||||
timeStart: 120000,
|
|
||||||
timeEnd: 720000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8222',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 900000,
|
|
||||||
type: 'delay',
|
|
||||||
revision: 0,
|
|
||||||
id: 'a386',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Add delay blocks to affect all events',
|
|
||||||
timeStart: 37320000,
|
|
||||||
timeEnd: 38520000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '6dce',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Add and remove events with [+] and [-]',
|
|
||||||
timeStart: 38520000,
|
|
||||||
timeEnd: 45120000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '2651',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: 'e6a1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'And control whether they are public',
|
|
||||||
timeStart: 46800000,
|
|
||||||
timeEnd: 57600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '1358',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 28800000,
|
|
||||||
timeEnd: 30600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
timeStart: 34920000 + 60000,
|
|
||||||
timeEnd: 35520000 + 60000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8ee5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Use simpler times to create a timer',
|
|
||||||
timeStart: 120000 + 60000,
|
|
||||||
timeEnd: 720000 + 60000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8222',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Add delay blocks to affect all events',
|
|
||||||
timeStart: 37320000 + 60000 + 900000,
|
|
||||||
timeEnd: 38520000 + 60000 + 900000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '6dce',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Add and remove events with [+] and [-]',
|
|
||||||
timeStart: 38520000 + 60000 + 900000,
|
|
||||||
timeEnd: 45120000 + 60000 + 900000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '2651',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'And control whether they are public',
|
|
||||||
timeStart: 46800000,
|
|
||||||
timeEnd: 57600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '1358',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
test('with negative delays', () => {
|
|
||||||
const testData = [
|
|
||||||
{
|
|
||||||
duration: -20,
|
|
||||||
type: 'delay',
|
|
||||||
id: '24240',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 100,
|
|
||||||
timeEnd: 200,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 80,
|
|
||||||
timeEnd: 180,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEventsWithDelay edge cases', () => {
|
|
||||||
it('ensures time start cannot be below 0', () => {
|
|
||||||
const testData = [
|
|
||||||
{
|
|
||||||
duration: -200,
|
|
||||||
type: 'delay',
|
|
||||||
id: '24240',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 20,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 0,
|
|
||||||
timeEnd: 0,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
it('does not modify original array', () => {
|
|
||||||
const testData = [
|
|
||||||
{
|
|
||||||
duration: 10,
|
|
||||||
type: 'delay',
|
|
||||||
id: '24240',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 20,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 20,
|
|
||||||
timeEnd: 30,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const expectedSafe = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 20,
|
|
||||||
timeEnd: 30,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
|
||||||
expect(getEventsWithDelay(expectedSafe)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('given an empty array', () => {
|
|
||||||
const emptyArray = {
|
|
||||||
test: [],
|
|
||||||
expect: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(emptyArray.test)).toStrictEqual(emptyArray.expect);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('given an undefined object', () => {
|
|
||||||
const withUndefined = {
|
|
||||||
test: undefined,
|
|
||||||
expect: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(withUndefined.test)).toStrictEqual(withUndefined.expect);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('given a corrupted event object', () => {
|
|
||||||
const testData = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeEnd: 30600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 60000,
|
|
||||||
type: 'delay',
|
|
||||||
id: '24240',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
timeStart: 34920000,
|
|
||||||
timeEnd: 35520000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8ee5',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeEnd: 30600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
timeStart: 34920000 + 60000,
|
|
||||||
timeEnd: 35520000 + 60000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8ee5',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('given a corrupted delay object', () => {
|
|
||||||
const testData = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 28800000,
|
|
||||||
timeEnd: 30600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'delay',
|
|
||||||
id: '24240',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
timeStart: 34920000,
|
|
||||||
timeEnd: 35520000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8ee5',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
timeStart: 28800000,
|
|
||||||
timeEnd: 30600000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
timeStart: 34920000,
|
|
||||||
timeEnd: 35520000,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
id: '8ee5',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('test trimEventlist function', () => {
|
|
||||||
const limit = 8;
|
|
||||||
const testData = [
|
|
||||||
{ id: '1' },
|
|
||||||
{ id: '2' },
|
|
||||||
{ id: '3' },
|
|
||||||
{ id: '4' },
|
|
||||||
{ id: '5' },
|
|
||||||
{ id: '6' },
|
|
||||||
{ id: '7' },
|
|
||||||
{ id: '8' },
|
|
||||||
{ id: '9' },
|
|
||||||
{ id: '10' },
|
|
||||||
{ id: '11' },
|
|
||||||
{ id: '12' },
|
|
||||||
];
|
|
||||||
|
|
||||||
it('when we use the first item', () => {
|
|
||||||
const selectedId = '1';
|
|
||||||
const expected = [
|
|
||||||
{ id: '1' },
|
|
||||||
{ id: '2' },
|
|
||||||
{ id: '3' },
|
|
||||||
{ id: '4' },
|
|
||||||
{ id: '5' },
|
|
||||||
{ id: '6' },
|
|
||||||
{ id: '7' },
|
|
||||||
{ id: '8' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const l = trimRundown(testData, selectedId, limit);
|
|
||||||
expect(l.length).toBe(limit);
|
|
||||||
expect(l).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when we use the third item', () => {
|
|
||||||
const selectedId = '3';
|
|
||||||
const expected = [
|
|
||||||
{ id: '1' },
|
|
||||||
{ id: '2' },
|
|
||||||
{ id: '3' },
|
|
||||||
{ id: '4' },
|
|
||||||
{ id: '5' },
|
|
||||||
{ id: '6' },
|
|
||||||
{ id: '7' },
|
|
||||||
{ id: '8' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const l = trimRundown(testData, selectedId, limit);
|
|
||||||
expect(l.length).toBe(limit);
|
|
||||||
expect(l).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when we use the fourth item', () => {
|
|
||||||
const selectedId = '4';
|
|
||||||
const expected = [
|
|
||||||
{ id: '2' },
|
|
||||||
{ id: '3' },
|
|
||||||
{ id: '4' },
|
|
||||||
{ id: '5' },
|
|
||||||
{ id: '6' },
|
|
||||||
{ id: '7' },
|
|
||||||
{ id: '8' },
|
|
||||||
{ id: '9' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const l = trimRundown(testData, selectedId, limit);
|
|
||||||
expect(l.length).toBe(limit);
|
|
||||||
expect(l).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if selected is not found', () => {
|
|
||||||
const selectedId = '15';
|
|
||||||
const expected = [
|
|
||||||
{ id: '1' },
|
|
||||||
{ id: '2' },
|
|
||||||
{ id: '3' },
|
|
||||||
{ id: '4' },
|
|
||||||
{ id: '5' },
|
|
||||||
{ id: '6' },
|
|
||||||
{ id: '7' },
|
|
||||||
{ id: '8' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const l = trimRundown(testData, selectedId, limit);
|
|
||||||
expect(l.length).toBe(limit);
|
|
||||||
expect(l).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('test formatEvents function', () => {
|
|
||||||
const testEvent = [
|
|
||||||
{
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
subtitle: 'Subtitles are useful',
|
|
||||||
presenter: 'cpvalente',
|
|
||||||
note: 'Maybe a running note for the operator?',
|
|
||||||
timeStart: 28800000,
|
|
||||||
timeEnd: 30600000,
|
|
||||||
isPublic: false,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: '5946',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: 'In green, below',
|
|
||||||
timeStart: 34800000,
|
|
||||||
timeEnd: 35400000,
|
|
||||||
isPublic: false,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: '8ee5',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('it parses correctly', () => {
|
|
||||||
const selectedId = 'otherEvent';
|
|
||||||
const nextId = 'notHere';
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
id: '5946',
|
|
||||||
time: '08:00 - 08:30',
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
isNow: false,
|
|
||||||
isNext: false,
|
|
||||||
colour: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8ee5',
|
|
||||||
time: '09:40 - 09:50',
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
isNow: false,
|
|
||||||
isNext: false,
|
|
||||||
colour: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
|
||||||
expect(parsed).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('it handles selected correctly', () => {
|
|
||||||
const selectedId = '5946';
|
|
||||||
const nextId = '8ee5';
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
id: '5946',
|
|
||||||
time: '08:00 - 08:30',
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
isNow: true,
|
|
||||||
isNext: false,
|
|
||||||
colour: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8ee5',
|
|
||||||
time: '09:40 - 09:50',
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
isNow: false,
|
|
||||||
isNext: true,
|
|
||||||
colour: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
|
||||||
expect(parsed).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('it handles next correctly', () => {
|
|
||||||
const selectedId = '8ee5';
|
|
||||||
const nextId = 'notHere';
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
id: '5946',
|
|
||||||
time: '08:00 - 08:30',
|
|
||||||
title: 'Welcome to Ontime',
|
|
||||||
isNow: false,
|
|
||||||
isNext: false,
|
|
||||||
colour: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8ee5',
|
|
||||||
time: '09:40 - 09:50',
|
|
||||||
title: 'Unless recalled by the OSC address',
|
|
||||||
isNow: true,
|
|
||||||
isNext: false,
|
|
||||||
colour: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
|
||||||
expect(parsed).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { EndAction, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
|
import { cloneEvent } from '../eventsManager';
|
||||||
|
|
||||||
|
describe('cloneEvent()', () => {
|
||||||
|
it('creates a stem from a given event', () => {
|
||||||
|
const original = {
|
||||||
|
id: 'unique',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
title: 'title',
|
||||||
|
cue: 'cue',
|
||||||
|
subtitle: 'subtitle',
|
||||||
|
presenter: 'presenter',
|
||||||
|
note: 'note',
|
||||||
|
timeStart: 0,
|
||||||
|
duration: 10,
|
||||||
|
timeEnd: 10,
|
||||||
|
timerType: TimerType.CountDown,
|
||||||
|
endAction: EndAction.None,
|
||||||
|
isPublic: false,
|
||||||
|
skip: false,
|
||||||
|
colour: 'F00',
|
||||||
|
revision: 10,
|
||||||
|
user0: 'user0',
|
||||||
|
user1: 'user1',
|
||||||
|
user2: 'user2',
|
||||||
|
user3: 'user3',
|
||||||
|
user4: 'user4',
|
||||||
|
user5: 'user5',
|
||||||
|
user6: 'user6',
|
||||||
|
user7: 'user7',
|
||||||
|
user8: 'user8',
|
||||||
|
user9: 'user9',
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
const cloned = cloneEvent(original);
|
||||||
|
expect(cloned).not.toBe(original);
|
||||||
|
// @ts-expect-error -- safeguarding this
|
||||||
|
expect(cloned?.id).toBe(undefined);
|
||||||
|
expect(cloned.title).toBe(original.title);
|
||||||
|
expect(cloned.subtitle).toBe(original.subtitle);
|
||||||
|
expect(cloned.presenter).toBe(original.presenter);
|
||||||
|
expect(cloned.note).toBe(original.note);
|
||||||
|
expect(cloned.endAction).toBe(original.endAction);
|
||||||
|
expect(cloned.timerType).toBe(original.timerType);
|
||||||
|
expect(cloned.timeStart).toBe(original.timeStart);
|
||||||
|
expect(cloned.timeEnd).toBe(original.timeEnd);
|
||||||
|
expect(cloned.duration).toBe(original.duration);
|
||||||
|
expect(cloned.isPublic).toBe(original.isPublic);
|
||||||
|
expect(cloned.skip).toBe(original.skip);
|
||||||
|
expect(cloned.colour).toBe(original.colour);
|
||||||
|
expect(cloned.type).toBe(SupportedEvent.Event);
|
||||||
|
expect(cloned.revision).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import getDelayTo from '../getDelayTo';
|
|
||||||
|
|
||||||
describe('getDelayTo function', () => {
|
|
||||||
it('handles list with delays', () => {
|
|
||||||
const delayDuration = 100;
|
|
||||||
const events = [
|
|
||||||
{ type: 'event' },
|
|
||||||
{ type: 'delay', duration: delayDuration },
|
|
||||||
{ type: 'event' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const notDelayed = getDelayTo(events, 0);
|
|
||||||
expect(notDelayed).toBe(0);
|
|
||||||
const delayedEvent = getDelayTo(events, 2);
|
|
||||||
expect(delayedEvent).toBe(delayDuration);
|
|
||||||
});
|
|
||||||
it('handles list without delays', () => {
|
|
||||||
const events = [{ type: 'event' }, { type: 'event' }];
|
|
||||||
const notDelayed = getDelayTo(events, 1);
|
|
||||||
expect(notDelayed).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles list with multiple delays', () => {
|
|
||||||
const delayDuration = 100;
|
|
||||||
const events = [
|
|
||||||
{ type: 'event' },
|
|
||||||
{ type: 'delay', duration: delayDuration },
|
|
||||||
{ type: 'event' },
|
|
||||||
{ type: 'delay', duration: delayDuration },
|
|
||||||
{ type: 'event' },
|
|
||||||
];
|
|
||||||
const doubleDelay = getDelayTo(events, 4);
|
|
||||||
expect(doubleDelay).toBe(delayDuration * 2);
|
|
||||||
});
|
|
||||||
it('handles list with blocks', () => {
|
|
||||||
const events = [
|
|
||||||
{ type: 'event' },
|
|
||||||
{ type: 'delay', duration: 100 },
|
|
||||||
{ type: 'event' },
|
|
||||||
{ type: 'block' },
|
|
||||||
{ type: 'event' },
|
|
||||||
];
|
|
||||||
const notDelayed = getDelayTo(events, 4);
|
|
||||||
expect(notDelayed).toBe(0);
|
|
||||||
});
|
|
||||||
it('handles index greater than list', () => {
|
|
||||||
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
|
||||||
const notDelayed = getDelayTo(events, 3);
|
|
||||||
expect(notDelayed).toBe(0);
|
|
||||||
});
|
|
||||||
it('handles negative index (not found)', () => {
|
|
||||||
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
|
||||||
const notDelayed = getDelayTo(events, -1);
|
|
||||||
expect(notDelayed).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Alias } from 'ontime-types';
|
|
||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import { Location, resolvePath } from 'react-router-dom';
|
import { Location, resolvePath } from 'react-router-dom';
|
||||||
|
import { Alias } from 'ontime-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates an alias against defined parameters
|
* Validates an alias against defined parameters
|
||||||
|
|||||||
@@ -1,174 +1,32 @@
|
|||||||
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { formatTime } from './time';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description From a list of events, returns only events of type event with calculated delays
|
|
||||||
* @param {Object[]} rundown - given rundown
|
|
||||||
* @returns {Object[]} Filtered events with calculated delays
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const getEventsWithDelay = (rundown: OntimeRundownEntry[]): OntimeEvent[] => {
|
|
||||||
if (rundown == null) return [];
|
|
||||||
|
|
||||||
const delayedEvents: OntimeEvent[] = [];
|
|
||||||
|
|
||||||
// Add running delay
|
|
||||||
let delay = 0;
|
|
||||||
for (const event of rundown) {
|
|
||||||
if (event.type === SupportedEvent.Block) delay = 0;
|
|
||||||
else if (event.type === SupportedEvent.Delay) {
|
|
||||||
if (typeof event.duration === 'number') {
|
|
||||||
delay += event.duration;
|
|
||||||
}
|
|
||||||
} else if (event.type === SupportedEvent.Event) {
|
|
||||||
const delayedEvent = { ...event };
|
|
||||||
if (delay !== 0) {
|
|
||||||
delayedEvent.timeStart = Math.max(delayedEvent.timeStart + delay, 0);
|
|
||||||
delayedEvent.timeEnd = Math.max(delayedEvent.timeEnd + delay, 0);
|
|
||||||
}
|
|
||||||
delayedEvents.push(delayedEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return delayedEvents;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Returns trimmed event list array
|
|
||||||
* @param {Object[]} rundown - given rundown
|
|
||||||
* @param {string} selectedId - id of currently selected event
|
|
||||||
* @param {number} limit - max number of events to return
|
|
||||||
* @returns {Object[]} Event list with maximum <limit> objects
|
|
||||||
*/
|
|
||||||
export const trimRundown = (rundown: OntimeEvent[], selectedId: string, limit: number): OntimeEvent[] => {
|
|
||||||
if (rundown == null) return [];
|
|
||||||
|
|
||||||
const BEFORE = 2;
|
|
||||||
const trimmedRundown = [...rundown];
|
|
||||||
|
|
||||||
// limit events length if necessary
|
|
||||||
if (limit != null) {
|
|
||||||
while (trimmedRundown.length > limit) {
|
|
||||||
const idx = trimmedRundown.findIndex((e) => e.id === selectedId);
|
|
||||||
if (idx <= BEFORE) {
|
|
||||||
trimmedRundown.pop();
|
|
||||||
} else {
|
|
||||||
trimmedRundown.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trimmedRundown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormatEventListOptionsProp = {
|
|
||||||
showEnd?: boolean;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @description Returns list of events formatted to be displayed
|
|
||||||
* @param {Object[]} rundown - given rundown
|
|
||||||
* @param {string} selectedId - id of currently selected event
|
|
||||||
* @param {string} nextId - id of next event
|
|
||||||
* @param {object} [options]
|
|
||||||
* @param {boolean} [options.showEnd] - whether to show the end time
|
|
||||||
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
|
|
||||||
*/
|
|
||||||
export const formatEventList = (
|
|
||||||
rundown: OntimeEvent[],
|
|
||||||
selectedId: string,
|
|
||||||
nextId: string,
|
|
||||||
options: FormatEventListOptionsProp,
|
|
||||||
): ScheduleEvent[] => {
|
|
||||||
if (rundown == null) return [];
|
|
||||||
const { showEnd = false } = options;
|
|
||||||
|
|
||||||
const givenEvents = [...rundown];
|
|
||||||
|
|
||||||
// format list
|
|
||||||
const formattedEvents = [];
|
|
||||||
for (const event of givenEvents) {
|
|
||||||
const start = formatTime(event.timeStart);
|
|
||||||
const end = formatTime(event.timeEnd);
|
|
||||||
|
|
||||||
formattedEvents.push({
|
|
||||||
id: event.id,
|
|
||||||
time: showEnd ? `${start} - ${end}` : start,
|
|
||||||
title: event.title,
|
|
||||||
isNow: event.id === selectedId,
|
|
||||||
isNext: event.id === nextId,
|
|
||||||
colour: event.colour,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedEvents;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ScheduleEvent = {
|
|
||||||
id: string;
|
|
||||||
time: string;
|
|
||||||
title: string;
|
|
||||||
isNow: boolean;
|
|
||||||
isNext: boolean;
|
|
||||||
colour: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Creates a safe duplicate of an event
|
* @description Creates a safe duplicate of an event
|
||||||
* @param {object} event
|
* @param {OntimeEvent} event
|
||||||
* @return {object} clean event
|
* @param {string} [after]
|
||||||
|
* @return {OntimeEvent} clean event
|
||||||
*/
|
*/
|
||||||
type ClonedEvent = OntimeEvent | { after?: string };
|
type ClonedEvent = Omit<
|
||||||
|
OntimeEvent,
|
||||||
|
'id' | 'user0' | 'user1' | 'user2' | 'user3' | 'user4' | 'user5' | 'user6' | 'user7' | 'user8' | 'user9'
|
||||||
|
>;
|
||||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||||
return {
|
return {
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
|
cue: event.cue,
|
||||||
subtitle: event.subtitle,
|
subtitle: event.subtitle,
|
||||||
presenter: event.presenter,
|
presenter: event.presenter,
|
||||||
note: event.note,
|
note: event.note,
|
||||||
timeStart: event.timeStart,
|
timeStart: event.timeStart,
|
||||||
|
duration: event.duration,
|
||||||
timeEnd: event.timeEnd,
|
timeEnd: event.timeEnd,
|
||||||
|
timerType: event.timerType,
|
||||||
|
endAction: event.endAction,
|
||||||
isPublic: event.isPublic,
|
isPublic: event.isPublic,
|
||||||
skip: event.skip,
|
skip: event.skip,
|
||||||
colour: event.colour,
|
colour: event.colour,
|
||||||
after: after,
|
after: after,
|
||||||
|
revision: 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets first event in rundown, if it exists
|
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
|
||||||
* @return {OntimeEvent | null}
|
|
||||||
*/
|
|
||||||
export function getFirstEvent(rundown: OntimeRundownEntry[]) {
|
|
||||||
return rundown.length ? rundown[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets next event in rundown, if it exists
|
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
|
||||||
* @param {string} currentId
|
|
||||||
* @return {OntimeEvent | null}
|
|
||||||
*/
|
|
||||||
export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string) {
|
|
||||||
const index = rundown.findIndex((event) => event.id === currentId);
|
|
||||||
if (index !== -1 && index + 1 < rundown.length) {
|
|
||||||
return rundown[index + 1];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets previous event in rundown, if it exists
|
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
|
||||||
* @param {string} currentId
|
|
||||||
* @return {OntimeEvent | null}
|
|
||||||
*/
|
|
||||||
export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: string) {
|
|
||||||
const index = rundown.findIndex((event) => event.id === currentId);
|
|
||||||
if (index !== -1 && index - 1 >= 0) {
|
|
||||||
return rundown[index - 1];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* @description calculates delay to a given event
|
|
||||||
* @param {array} events
|
|
||||||
* @param {number} eventIndex
|
|
||||||
* @return {number} - delay value of given event
|
|
||||||
*/
|
|
||||||
export default function getDelayTo(events, eventIndex) {
|
|
||||||
let delay = 0;
|
|
||||||
let index = 0;
|
|
||||||
if (eventIndex >= 0) {
|
|
||||||
for (const event of events) {
|
|
||||||
if (eventIndex === index) {
|
|
||||||
return delay;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'delay') {
|
|
||||||
delay += event.duration;
|
|
||||||
} else if (event.type === 'block') {
|
|
||||||
delay = 0;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Log, RuntimeStore } from 'ontime-types';
|
import { Log, RuntimeStore } from 'ontime-types';
|
||||||
|
|
||||||
import { RUNTIME, websocketUrl } from '../api/apiConstants';
|
import { isProduction, RUNTIME, websocketUrl } from '../api/apiConstants';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
import { socketClientName } from '../stores/connectionName';
|
import { socketClientName } from '../stores/connectionName';
|
||||||
import { addLog } from '../stores/logger';
|
import { addLog } from '../stores/logger';
|
||||||
@@ -64,7 +64,7 @@ export const connectSocket = (preferredClientName?: string) => {
|
|||||||
}
|
}
|
||||||
case 'ontime': {
|
case 'ontime': {
|
||||||
runtime.setState(payload as RuntimeStore);
|
runtime.setState(payload as RuntimeStore);
|
||||||
if (import.meta.env.DEV) {
|
if (!isProduction) {
|
||||||
ontimeQueryClient.setQueryData(RUNTIME, data.payload);
|
ontimeQueryClient.setQueryData(RUNTIME, data.payload);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
|
import { ComponentType, useEffect } from 'react';
|
||||||
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import useAliases from '../common/hooks-query/useAliases';
|
import useAliases from '../common/hooks-query/useAliases';
|
||||||
import { getAliasRoute } from '../common/utils/aliases';
|
import { getAliasRoute } from '../common/utils/aliases';
|
||||||
import { ComponentType, useEffect } from 'react';
|
|
||||||
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
const withAlias = <P extends object>(Component: ComponentType<P>) => {
|
const withAlias = <P extends object>(Component: ComponentType<P>) => {
|
||||||
return (props: Partial<P>) => {
|
return (props: Partial<P>) => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ function MakeUserField({ getValue, row: { index }, column: { id }, table }: Cell
|
|||||||
|
|
||||||
export function makeCuesheetColumns(userFields?: UserFields): ColumnDef<OntimeRundownEntry>[] {
|
export function makeCuesheetColumns(userFields?: UserFields): ColumnDef<OntimeRundownEntry>[] {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'cue',
|
||||||
|
id: 'cue',
|
||||||
|
header: 'Cue',
|
||||||
|
cell: (row) => row.getValue(),
|
||||||
|
size: 75,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'isPublic',
|
accessorKey: 'isPublic',
|
||||||
id: 'isPublic',
|
id: 'isPublic',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { OntimeEntryCommonKeys, OntimeEvent } from 'ontime-types';
|
|||||||
*/
|
*/
|
||||||
export const defaultColumnOrder: OntimeEntryCommonKeys[] = [
|
export const defaultColumnOrder: OntimeEntryCommonKeys[] = [
|
||||||
'isPublic',
|
'isPublic',
|
||||||
|
'cue',
|
||||||
'timeStart',
|
'timeStart',
|
||||||
'timeEnd',
|
'timeEnd',
|
||||||
'duration',
|
'duration',
|
||||||
|
|||||||
@@ -215,6 +215,11 @@ $playback-width: 26rem;
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mainContainer > .rundown {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding-top: 1.5rem;
|
padding-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,14 @@
|
|||||||
gap: max(1rem, 2vh);
|
gap: max(1rem, 2vh);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas: 'time left right';
|
||||||
'eventInfo eventActions'
|
grid-template-columns: auto 1fr 1fr;
|
||||||
'timeOptions titles';
|
}
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
|
.timeOptions {
|
||||||
|
grid-area: time;
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
.timers,
|
.timers,
|
||||||
.timeSettings {
|
.timeSettings {
|
||||||
@@ -19,51 +23,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventInfo {
|
.left,
|
||||||
grid-area: eventInfo;
|
.right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
.eventId {
|
|
||||||
margin-left:$element-spacing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventActions {
|
.left {
|
||||||
grid-area: eventActions;
|
grid-area: left;
|
||||||
margin-left: auto;
|
padding: 0 1rem;
|
||||||
|
border-left: 1px solid $border-color-ondark;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeOptions {
|
.right {
|
||||||
grid-area: timeOptions;
|
padding-left: 1rem;
|
||||||
display: flex;
|
grid-area: right;
|
||||||
gap: 1.5rem;
|
border-left: 1px solid $border-color-ondark;
|
||||||
}
|
|
||||||
|
|
||||||
.titles {
|
|
||||||
grid-area: titles;
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: 'left right';
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
|
|
||||||
.left,
|
|
||||||
.right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left {
|
|
||||||
grid-area: left;
|
|
||||||
padding: 0 1rem;
|
|
||||||
border-left: 1px solid $border-color-ondark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
padding-left: 1rem;
|
|
||||||
grid-area: right;
|
|
||||||
border-left: 1px solid $border-color-ondark;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin input-label() {
|
@mixin input-label() {
|
||||||
@@ -94,6 +70,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eventActions {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
}
|
}
|
||||||
@@ -104,6 +86,12 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splitTwo {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -116,4 +104,4 @@
|
|||||||
|
|
||||||
.fullHeight {
|
.fullHeight {
|
||||||
height: 100%
|
height: 100%
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,45 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { OntimeEvent } 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 useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
import { useAppMode } from '../../common/stores/appModeStore';
|
import { useAppMode } from '../../common/stores/appModeStore';
|
||||||
import getDelayTo from '../../common/utils/getDelayTo';
|
|
||||||
|
|
||||||
|
import EventEditorDataLeft from './composite/EventEditorDataLeft';
|
||||||
|
import EventEditorDataRight from './composite/EventEditorDataRight';
|
||||||
import EventEditorTimes from './composite/EventEditorTimes';
|
import EventEditorTimes from './composite/EventEditorTimes';
|
||||||
import EventEditorTitles from './composite/EventEditorTitles';
|
|
||||||
|
|
||||||
import style from './EventEditor.module.scss';
|
import style from './EventEditor.module.scss';
|
||||||
|
|
||||||
export type EventEditorSubmitActions = keyof OntimeEvent;
|
export type EventEditorSubmitActions = keyof OntimeEvent;
|
||||||
|
export type EditorUpdateFields = 'cue' | 'title' | 'presenter' | 'subtitle' | 'note' | 'colour';
|
||||||
|
|
||||||
export default function EventEditor() {
|
export default function EventEditor() {
|
||||||
const openId = useAppMode((state) => state.editId);
|
const openId = useAppMode((state) => state.editId);
|
||||||
const { data } = useRundown();
|
const { data } = useRundown();
|
||||||
|
const { updateEvent } = useEventAction();
|
||||||
|
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||||
const [delay, setDelay] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || !openId) {
|
if (!data || !openId) {
|
||||||
|
setEvent(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventIndex = data.findIndex((event) => event.id === openId);
|
const event = data.find((event) => event.id === openId);
|
||||||
if (eventIndex > -1) {
|
if (event && isOntimeEvent(event)) {
|
||||||
const event = data[eventIndex];
|
setEvent(event);
|
||||||
if (event.type === 'event') {
|
|
||||||
setDelay(getDelayTo(data, eventIndex));
|
|
||||||
setEvent(data[eventIndex] as OntimeEvent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [data, event, openId]);
|
}, [data, openId]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(field: EditorUpdateFields, value: string) => {
|
||||||
|
updateEvent({ id: event?.id, [field]: value });
|
||||||
|
},
|
||||||
|
[event?.id, updateEvent],
|
||||||
|
);
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return <span>Loading...</span>;
|
return <span>Loading...</span>;
|
||||||
@@ -40,34 +47,35 @@ export default function EventEditor() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.eventEditor}>
|
<div className={style.eventEditor}>
|
||||||
<div className={style.eventInfo}>
|
|
||||||
Event ID
|
|
||||||
<span className={style.eventId}>
|
|
||||||
<CopyTag label={event.id}>{event.id}</CopyTag>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={style.eventActions}>
|
|
||||||
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
|
||||||
</div>
|
|
||||||
<EventEditorTimes
|
<EventEditorTimes
|
||||||
eventId={event.id}
|
eventId={event.id}
|
||||||
timeStart={event.timeStart}
|
timeStart={event.timeStart}
|
||||||
timeEnd={event.timeEnd}
|
timeEnd={event.timeEnd}
|
||||||
duration={event.duration}
|
duration={event.duration}
|
||||||
delay={delay}
|
delay={event.delay ?? 0}
|
||||||
isPublic={event.isPublic}
|
isPublic={event.isPublic}
|
||||||
endAction={event.endAction}
|
endAction={event.endAction}
|
||||||
timerType={event.timerType}
|
timerType={event.timerType}
|
||||||
/>
|
/>
|
||||||
<EventEditorTitles
|
<EventEditorDataLeft
|
||||||
key={event.id}
|
key={`${event.id}-left`}
|
||||||
eventId={event.id}
|
eventId={event.id}
|
||||||
|
cue={event.cue}
|
||||||
title={event.title}
|
title={event.title}
|
||||||
presenter={event.presenter}
|
presenter={event.presenter}
|
||||||
subtitle={event.subtitle}
|
subtitle={event.subtitle}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
<EventEditorDataRight
|
||||||
|
key={`${event.id}-right`}
|
||||||
note={event.note}
|
note={event.note}
|
||||||
colour={event.colour}
|
colour={event.colour}
|
||||||
/>
|
handleSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<CopyTag label='Event ID'>{event.id}</CopyTag>
|
||||||
|
<CopyTag label='OSC trigger by id'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
||||||
|
<CopyTag label='OSC trigger by cue'>{`/ontime/gotocue/${event.cue}`}</CopyTag>
|
||||||
|
</EventEditorDataRight>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Textarea } from '@chakra-ui/react';
|
|||||||
|
|
||||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
|
||||||
import { TitleActions } from './EventEditorTitles';
|
import { TitleActions } from './EventEditorDataLeft';
|
||||||
|
|
||||||
import style from '../EventEditor.module.scss';
|
import style from '../EventEditor.module.scss';
|
||||||
|
|
||||||
@@ -24,7 +24,9 @@ export default function CountedTextArea(props: CountedTextAreaProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`${style.column} ${style.fullHeight}`}>
|
<div className={`${style.column} ${style.fullHeight}`}>
|
||||||
<div className={style.countedInput}>
|
<div className={style.countedInput}>
|
||||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
<label className={style.inputLabel} htmlFor={field}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Input } from '@chakra-ui/react';
|
import { Input, InputProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
import { EditorUpdateFields } from '../EventEditor';
|
||||||
import { TitleActions } from './EventEditorTitles';
|
|
||||||
|
|
||||||
import style from '../EventEditor.module.scss';
|
import style from '../EventEditor.module.scss';
|
||||||
|
|
||||||
interface CountedTextInputProps {
|
interface CountedTextInputProps extends InputProps {
|
||||||
field: TitleActions;
|
field: EditorUpdateFields;
|
||||||
label: string;
|
label: string;
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
submitHandler: (field: TitleActions, value: string) => void;
|
submitHandler: (field: EditorUpdateFields, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CountedTextInput(props: CountedTextInputProps) {
|
export default function CountedTextInput(props: CountedTextInputProps) {
|
||||||
const { field, label, initialValue, submitHandler } = props;
|
const { field, label, initialValue, submitHandler, maxLength } = props;
|
||||||
|
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
|
|
||||||
@@ -26,7 +25,9 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<div className={style.countedInput}>
|
<div className={style.countedInput}>
|
||||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
<label className={style.inputLabel} htmlFor={field}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -35,6 +36,7 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
|||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
data-testid='input-textfield'
|
data-testid='input-textfield'
|
||||||
value={value}
|
value={value}
|
||||||
|
maxLength={maxLength || 50}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { Input } from '@chakra-ui/react';
|
||||||
|
import { sanitiseCue } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { type EditorUpdateFields } from '../EventEditor';
|
||||||
|
|
||||||
|
import CountedTextInput from './CountedTextInput';
|
||||||
|
|
||||||
|
import style from '../EventEditor.module.scss';
|
||||||
|
|
||||||
|
interface EventEditorLeftProps {
|
||||||
|
eventId: string;
|
||||||
|
cue: string;
|
||||||
|
title: string;
|
||||||
|
presenter: string;
|
||||||
|
subtitle: string;
|
||||||
|
handleSubmit: (field: EditorUpdateFields, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventEditorDataLeft = (props: EventEditorLeftProps) => {
|
||||||
|
const { eventId, cue, title, presenter, subtitle, handleSubmit } = props;
|
||||||
|
|
||||||
|
const cueSubmitHandler = (_field: string, newValue: string) => {
|
||||||
|
handleSubmit('cue', sanitiseCue(newValue));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.left}>
|
||||||
|
<div className={style.splitTwo}>
|
||||||
|
<div className={style.column}>
|
||||||
|
<div className={style.countedInput}>
|
||||||
|
<label className={style.inputLabel} htmlFor='eventId'>
|
||||||
|
Event ID (read only)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id='eventId'
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
data-testid='input-textfield'
|
||||||
|
value={eventId}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CountedTextInput field='cue' label='Cue' initialValue={cue} submitHandler={cueSubmitHandler} maxLength={10} />
|
||||||
|
</div>
|
||||||
|
<CountedTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
||||||
|
<CountedTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
|
||||||
|
<CountedTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(EventEditorDataLeft);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { memo, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||||
|
import { EditorUpdateFields } from '../EventEditor';
|
||||||
|
|
||||||
|
import CountedTextArea from './CountedTextArea';
|
||||||
|
|
||||||
|
import style from '../EventEditor.module.scss';
|
||||||
|
|
||||||
|
interface EventEditorRightProps {
|
||||||
|
note: string;
|
||||||
|
colour: string;
|
||||||
|
handleSubmit: (field: EditorUpdateFields, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventEditorDataRight = (props: PropsWithChildren<EventEditorRightProps>) => {
|
||||||
|
const { children, note, colour, handleSubmit } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.right}>
|
||||||
|
<div className={style.column}>
|
||||||
|
<label className={style.inputLabel}>Colour</label>
|
||||||
|
<div className={style.inline}>
|
||||||
|
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CountedTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
||||||
|
<div className={style.eventActions}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(EventEditorDataRight);
|
||||||
@@ -130,6 +130,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
|||||||
>
|
>
|
||||||
<option value={TimerType.CountDown}>Count down</option>
|
<option value={TimerType.CountDown}>Count down</option>
|
||||||
<option value={TimerType.CountUp}>Count up</option>
|
<option value={TimerType.CountUp}>Count up</option>
|
||||||
|
<option value={TimerType.TimeToEnd}>Time to end</option>
|
||||||
<option value={TimerType.Clock}>Clock</option>
|
<option value={TimerType.Clock}>Clock</option>
|
||||||
</Select>
|
</Select>
|
||||||
<label className={style.inputLabel}>End Action</label>
|
<label className={style.inputLabel}>End Action</label>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
|
||||||
|
|
||||||
import CountedTextArea from './CountedTextArea';
|
|
||||||
import CountedTextInput from './CountedTextInput';
|
|
||||||
|
|
||||||
import style from '../EventEditor.module.scss';
|
|
||||||
|
|
||||||
interface EventEditorTitlesProps {
|
|
||||||
eventId: string;
|
|
||||||
title: string;
|
|
||||||
presenter: string;
|
|
||||||
subtitle: string;
|
|
||||||
note: string;
|
|
||||||
colour: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TitleActions = 'title' | 'presenter' | 'subtitle' | 'note' | 'colour';
|
|
||||||
|
|
||||||
const EventEditorTitles = (props: EventEditorTitlesProps) => {
|
|
||||||
const { eventId, title, presenter, subtitle, note, colour } = props;
|
|
||||||
const { updateEvent } = useEventAction();
|
|
||||||
|
|
||||||
const handleSubmit = (field: TitleActions, value: string) => {
|
|
||||||
updateEvent({ id: eventId, [field]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.titles}>
|
|
||||||
<div className={style.left}>
|
|
||||||
<CountedTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
|
||||||
<CountedTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
|
|
||||||
<CountedTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
|
|
||||||
</div>
|
|
||||||
<div className={style.right}>
|
|
||||||
<div className={style.column}>
|
|
||||||
<label className={style.inputLabel}>Colour</label>
|
|
||||||
<div className={style.inline}>
|
|
||||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<CountedTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(EventEditorTitles);
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
|
|
||||||
|
import { serverPort } from '../../common/api/apiConstants';
|
||||||
import useInfo from '../../common/hooks-query/useInfo';
|
import useInfo from '../../common/hooks-query/useInfo';
|
||||||
import { openLink } from '../../common/utils/linkUtils';
|
import { openLink } from '../../common/utils/linkUtils';
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ export default function InfoNif() {
|
|||||||
const { data } = useInfo();
|
const { data } = useInfo();
|
||||||
|
|
||||||
const handleClick = (address: string) => {
|
const handleClick = (address: string) => {
|
||||||
const baseURL = 'http://__IP__:4001';
|
const baseURL = `http://__IP__:${serverPort}`;
|
||||||
openLink(baseURL.replace('__IP__', address));
|
openLink(baseURL.replace('__IP__', address));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.headerButtons {
|
.headerButtons {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-top: 24px;
|
padding: 1.5rem 1rem 0 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function AppSettingsModal() {
|
|||||||
<ModalSplitInput
|
<ModalSplitInput
|
||||||
field='serverPort'
|
field='serverPort'
|
||||||
title='Ontime is available on port'
|
title='Ontime is available on port'
|
||||||
description='Default 4001'
|
description='Default 4001 (needs app restart to change)'
|
||||||
error={errors.serverPort?.message}
|
error={errors.serverPort?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@@ -69,7 +69,6 @@ export default function AppSettingsModal() {
|
|||||||
size='sm'
|
size='sm'
|
||||||
textAlign='right'
|
textAlign='right'
|
||||||
maxLength={5}
|
maxLength={5}
|
||||||
disabled
|
|
||||||
variant='ontime-filled-on-light'
|
variant='ontime-filled-on-light'
|
||||||
{...register('serverPort', {
|
{...register('serverPort', {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
@use '../../theme/v2Styles' as *;
|
||||||
|
|
||||||
.eventContainer {
|
.eventContainer {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 8px 4px 8px 0;
|
padding: 0 4px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -22,11 +26,29 @@
|
|||||||
.alignCenter {
|
.alignCenter {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.spaceTop {
|
.spaceTop {
|
||||||
margin-top: 24px;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
min-height: 50vh;
|
min-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entryWrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entryIndex {
|
||||||
|
text-align: right;
|
||||||
|
min-width: 2em;
|
||||||
|
color: $label-gray;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { lazy, MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
|
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
|
||||||
|
import { getFirst, getNext, getPrevious } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { useEditorSettings } from '../../common/stores/editorSettings';
|
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||||
import { cloneEvent, getFirstEvent, getNextEvent, getPreviousEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||||
|
|
||||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||||
import RundownEmpty from './RundownEmpty';
|
import RundownEmpty from './RundownEmpty';
|
||||||
@@ -59,8 +60,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
if (type === 'clone') {
|
if (type === 'clone') {
|
||||||
const cursorEvent = entries.find((event) => event.id === cursor);
|
const cursorEvent = entries.find((event) => event.id === cursor);
|
||||||
if (cursorEvent?.type === SupportedEvent.Event) {
|
if (cursorEvent?.type === SupportedEvent.Event) {
|
||||||
const newEvent = cloneEvent(cursorEvent);
|
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
||||||
newEvent.after = cursorEvent.id;
|
|
||||||
addEvent(newEvent);
|
addEvent(newEvent);
|
||||||
}
|
}
|
||||||
} else if (type === SupportedEvent.Event) {
|
} else if (type === SupportedEvent.Event) {
|
||||||
@@ -93,7 +93,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
if (entries.length < 1) {
|
if (entries.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextEvent = cursor == null ? getFirstEvent(entries) : getNextEvent(entries, cursor);
|
const nextEvent = cursor == null ? getFirst(entries) : getNext(entries, cursor);
|
||||||
if (nextEvent) {
|
if (nextEvent) {
|
||||||
moveCursorTo(nextEvent.id, nextEvent.type === SupportedEvent.Event);
|
moveCursorTo(nextEvent.id, nextEvent.type === SupportedEvent.Event);
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
||||||
const previousEvent = cursor == null ? getFirstEvent(entries) : getPreviousEvent(entries, cursor);
|
const previousEvent = cursor == null ? getFirst(entries) : getPrevious(entries, cursor);
|
||||||
if (previousEvent) {
|
if (previousEvent) {
|
||||||
moveCursorTo(previousEvent.id, previousEvent.type === SupportedEvent.Event);
|
moveCursorTo(previousEvent.id, previousEvent.type === SupportedEvent.Event);
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
let previousEnd = 0;
|
let previousEnd = 0;
|
||||||
let thisEnd = 0;
|
let thisEnd = 0;
|
||||||
let previousEventId: string | undefined;
|
let previousEventId: string | undefined;
|
||||||
let eventIndex = -1;
|
let eventIndex = 0;
|
||||||
let isPast = Boolean(featureData?.selectedEventId);
|
let isPast = Boolean(featureData?.selectedEventId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -216,7 +216,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
{statefulEntries.map((entry, index) => {
|
{statefulEntries.map((entry, index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
eventIndex = -1;
|
eventIndex = 0;
|
||||||
}
|
}
|
||||||
if (entry.type === SupportedEvent.Event) {
|
if (entry.type === SupportedEvent.Event) {
|
||||||
eventIndex++;
|
eventIndex++;
|
||||||
@@ -233,21 +233,25 @@ export default function Rundown(props: RundownProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
<Fragment key={entry.id}>
|
||||||
<RundownEntry
|
<div className={style.entryWrapper} data-testid={`entry-${eventIndex}`}>
|
||||||
type={entry.type}
|
{entry.type === SupportedEvent.Event && <div className={style.entryIndex}>{eventIndex}</div>}
|
||||||
eventIndex={eventIndex}
|
<div className={style.entry} key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
||||||
isPast={isPast}
|
<RundownEntry
|
||||||
data={entry}
|
type={entry.type}
|
||||||
selected={isSelected}
|
isPast={isPast}
|
||||||
hasCursor={hasCursor}
|
data={entry}
|
||||||
next={isNext}
|
selected={isSelected}
|
||||||
previousEnd={previousEnd}
|
hasCursor={hasCursor}
|
||||||
previousEventId={previousEventId}
|
next={isNext}
|
||||||
playback={isSelected ? featureData.playback : undefined}
|
previousEnd={previousEnd}
|
||||||
isRolling={featureData.playback === Playback.Roll}
|
previousEventId={previousEventId}
|
||||||
disableEdit={isExtracted}
|
playback={isSelected ? featureData.playback : undefined}
|
||||||
/>
|
isRolling={featureData.playback === Playback.Roll}
|
||||||
|
disableEdit={isExtracted}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{((showQuickEntry && hasCursor) || isLast) && (
|
{((showQuickEntry && hasCursor) || isLast) && (
|
||||||
<QuickAddBlock
|
<QuickAddBlock
|
||||||
showKbd={hasCursor}
|
showKbd={hasCursor}
|
||||||
@@ -257,7 +261,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
disableAddBlock={entry.type === SupportedEvent.Block}
|
disableAddBlock={entry.type === SupportedEvent.Block}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className={style.spacer} />
|
<div className={style.spacer} />
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||||
import { calculateDuration } from 'ontime-utils';
|
import { calculateDuration, getCueCandidate } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { RUNDOWN_TABLE } from '../../common/api/apiConstants';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
import { useAppMode } from '../../common/stores/appModeStore';
|
import { useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { useEditorSettings } from '../../common/stores/editorSettings';
|
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||||
import { useEmitLog } from '../../common/stores/logger';
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
@@ -12,11 +14,10 @@ import BlockBlock from './block-block/BlockBlock';
|
|||||||
import DelayBlock from './delay-block/DelayBlock';
|
import DelayBlock from './delay-block/DelayBlock';
|
||||||
import EventBlock from './event-block/EventBlock';
|
import EventBlock from './event-block/EventBlock';
|
||||||
|
|
||||||
export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'delete' | 'clone' | 'update';
|
export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'delete' | 'clone' | 'update' | 'swap';
|
||||||
|
|
||||||
interface RundownEntryProps {
|
interface RundownEntryProps {
|
||||||
type: SupportedEvent;
|
type: SupportedEvent;
|
||||||
eventIndex: number;
|
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
data: OntimeRundownEntry;
|
data: OntimeRundownEntry;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@@ -30,21 +31,10 @@ interface RundownEntryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RundownEntry(props: RundownEntryProps) {
|
export default function RundownEntry(props: RundownEntryProps) {
|
||||||
const {
|
const { isPast, data, selected, hasCursor, next, previousEnd, previousEventId, playback, isRolling, disableEdit } =
|
||||||
eventIndex,
|
props;
|
||||||
isPast,
|
|
||||||
data,
|
|
||||||
selected,
|
|
||||||
hasCursor,
|
|
||||||
next,
|
|
||||||
previousEnd,
|
|
||||||
previousEventId,
|
|
||||||
playback,
|
|
||||||
isRolling,
|
|
||||||
disableEdit,
|
|
||||||
} = props;
|
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { addEvent, updateEvent, deleteEvent } = useEventAction();
|
const { addEvent, updateEvent, deleteEvent, swapEvents } = useEventAction();
|
||||||
|
|
||||||
const cursor = useAppMode((state) => state.cursor);
|
const cursor = useAppMode((state) => state.cursor);
|
||||||
const setCursor = useAppMode((state) => state.setCursor);
|
const setCursor = useAppMode((state) => state.setCursor);
|
||||||
@@ -95,6 +85,12 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
addEvent({ type: SupportedEvent.Block }, { after: data.id });
|
addEvent({ type: SupportedEvent.Block }, { after: data.id });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'swap': {
|
||||||
|
const { value } = payload as FieldValue;
|
||||||
|
swapEvents({ from: value as string, to: data.id });
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
if (openId === data.id) {
|
if (openId === data.id) {
|
||||||
removeOpenEvent();
|
removeOpenEvent();
|
||||||
@@ -104,6 +100,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
}
|
}
|
||||||
case 'clone': {
|
case 'clone': {
|
||||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
||||||
|
newEvent.cue = getCueCandidate(ontimeQueryClient.getQueryData(RUNDOWN_TABLE) || [], data.id);
|
||||||
addEvent(newEvent);
|
addEvent(newEvent);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -149,16 +146,17 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
removeOpenEvent,
|
removeOpenEvent,
|
||||||
startTimeIsLastEnd,
|
startTimeIsLastEnd,
|
||||||
updateEvent,
|
updateEvent,
|
||||||
|
swapEvents,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data.type === SupportedEvent.Event) {
|
if (data.type === SupportedEvent.Event) {
|
||||||
return (
|
return (
|
||||||
<EventBlock
|
<EventBlock
|
||||||
|
cue={data.cue}
|
||||||
timeStart={data.timeStart}
|
timeStart={data.timeStart}
|
||||||
timeEnd={data.timeEnd}
|
timeEnd={data.timeEnd}
|
||||||
duration={data.duration}
|
duration={data.duration}
|
||||||
eventIndex={eventIndex + 1}
|
|
||||||
eventId={data.id}
|
eventId={data.id}
|
||||||
isPublic={data.isPublic}
|
isPublic={data.isPublic}
|
||||||
endAction={data.endAction}
|
endAction={data.endAction}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
|
|
||||||
background-color: $block-bg2;
|
background-color: $block-bg2;
|
||||||
|
|
||||||
|
min-width: 34rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px 1fr auto;
|
grid-template-columns: 2rem 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: $secondary-block-height;
|
height: $secondary-block-height;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
|
|
||||||
&.hasCursor {
|
&.hasCursor {
|
||||||
outline: 1px solid $block-cursor-color;
|
outline: 1px solid $block-cursor-color;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function BlockBlock(props: BlockBlockProps) {
|
|||||||
<IoReorderTwo />
|
<IoReorderTwo />
|
||||||
</span>
|
</span>
|
||||||
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
||||||
<BlockActionMenu className={style.actionMenu} showAdd showDelay enableDelete actionHandler={actionHandler} />
|
<BlockActionMenu className={style.actionMenu} enableDelete actionHandler={actionHandler} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
|
|
||||||
background-color: $block-bg2;
|
background-color: $block-bg2;
|
||||||
|
|
||||||
|
min-width: 34rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px 1fr auto;
|
grid-template-columns: 2rem 1fr auto;
|
||||||
grid-template-areas: 'drag inpt btns';
|
grid-template-areas: 'drag inpt btns';
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: $secondary-block-height;
|
height: $secondary-block-height;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
|
|
||||||
&.hasCursor {
|
&.hasCursor {
|
||||||
outline: 1px solid $block-cursor-color;
|
outline: 1px solid $block-cursor-color;
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function DelayBlock(props: DelayBlockProps) {
|
|||||||
<Button onClick={cancelDelayHandler} size='sm' leftIcon={<IoClose />} variant='ontime-subtle-white'>
|
<Button onClick={cancelDelayHandler} size='sm' leftIcon={<IoClose />} variant='ontime-subtle-white'>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<BlockActionMenu showAdd enableDelete actionHandler={actionHandler} />
|
<BlockActionMenu enableDelete actionHandler={actionHandler} />
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,6 +90,18 @@ $skip-opacity: 0.1;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cue {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 5.5rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
rotate: -90deg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.playbackActions {
|
.playbackActions {
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { IoCopyOutline } from '@react-icons/all-files/io5/IoCopyOutline';
|
import { IoCopyOutline } from '@react-icons/all-files/io5/IoCopyOutline';
|
||||||
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
|
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
|
||||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||||
|
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||||
import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types';
|
import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
|
||||||
import { useAppMode } from '../../../common/stores/appModeStore';
|
import { useAppMode } from '../../../common/stores/appModeStore';
|
||||||
import copyToClipboard from '../../../common/utils/copyToClipboard';
|
import copyToClipboard from '../../../common/utils/copyToClipboard';
|
||||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
import type { EventItemActions } from '../RundownEntry';
|
import type { EventItemActions } from '../RundownEntry';
|
||||||
|
import { useEventIdSwapping } from '../useEventIdSwapping';
|
||||||
|
|
||||||
import EventBlockInner from './EventBlockInner';
|
import EventBlockInner from './EventBlockInner';
|
||||||
|
|
||||||
import style from './EventBlock.module.scss';
|
import style from './EventBlock.module.scss';
|
||||||
|
|
||||||
interface EventBlockProps {
|
interface EventBlockProps {
|
||||||
|
cue: string;
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
timeEnd: number;
|
timeEnd: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
eventIndex: number;
|
|
||||||
eventId: string;
|
eventId: string;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
endAction: EndAction;
|
endAction: EndAction;
|
||||||
@@ -52,11 +54,11 @@ interface EventBlockProps {
|
|||||||
|
|
||||||
export default function EventBlock(props: EventBlockProps) {
|
export default function EventBlock(props: EventBlockProps) {
|
||||||
const {
|
const {
|
||||||
|
eventId,
|
||||||
|
cue,
|
||||||
timeStart,
|
timeStart,
|
||||||
timeEnd,
|
timeEnd,
|
||||||
duration,
|
duration,
|
||||||
eventIndex,
|
|
||||||
eventId,
|
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
endAction,
|
endAction,
|
||||||
timerType,
|
timerType,
|
||||||
@@ -75,22 +77,37 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
actionHandler,
|
actionHandler,
|
||||||
disableEdit,
|
disableEdit,
|
||||||
} = props;
|
} = props;
|
||||||
const { updateEvent } = useEventAction();
|
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||||
const moveCursorTo = useAppMode((state) => state.setCursor);
|
const moveCursorTo = useAppMode((state) => state.setCursor);
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const openId = useAppMode((state) => state.editId);
|
const openId = useAppMode((state) => state.editId);
|
||||||
const [onContextMenu] = useContextMenu<HTMLDivElement>([
|
const [onContextMenu] = useContextMenu<HTMLDivElement>([
|
||||||
{ label: `Copy ID: ${eventId}}`, icon: IoCopyOutline, onClick: () => copyToClipboard(eventId) },
|
{ label: `Copy ID: ${eventId}`, icon: IoCopyOutline, onClick: () => copyToClipboard(eventId) },
|
||||||
{
|
{
|
||||||
label: 'Toggle public',
|
label: 'Toggle public',
|
||||||
icon: IoPeopleOutline,
|
icon: IoPeopleOutline,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
updateEvent({
|
actionHandler('update', {
|
||||||
id: eventId,
|
field: 'isPublic',
|
||||||
isPublic: !isPublic,
|
value: !isPublic,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Add to swap',
|
||||||
|
icon: IoAdd,
|
||||||
|
onClick: () => setSelectedEventId(eventId),
|
||||||
|
withDivider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Swap this event with ${selectedEventId ?? ''}`,
|
||||||
|
icon: IoSwapVertical,
|
||||||
|
onClick: () => {
|
||||||
|
actionHandler('swap', { field: 'id', value: selectedEventId });
|
||||||
|
clearSelectedEventId();
|
||||||
|
},
|
||||||
|
isDisabled: selectedEventId == null || selectedEventId === eventId,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -170,7 +187,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||||
<IoReorderTwo />
|
<IoReorderTwo />
|
||||||
</span>
|
</span>
|
||||||
{eventIndex}
|
<span className={style.cue}>{cue}</span>
|
||||||
</div>
|
</div>
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<EventBlockInner
|
<EventBlockInner
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
import { BiArrowToBottom } from '@react-icons/all-files/bi/BiArrowToBottom';
|
||||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||||
@@ -144,7 +145,10 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
|
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
|
||||||
<span>
|
<span>
|
||||||
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} />
|
<IoPeople
|
||||||
|
className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`}
|
||||||
|
data-isPublic={isPublic}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +167,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
color={isOpen ? 'white' : '#f6f6f6'}
|
color={isOpen ? 'white' : '#f6f6f6'}
|
||||||
isDisabled={disableEdit}
|
isDisabled={disableEdit}
|
||||||
/>
|
/>
|
||||||
<BlockActionMenu showAdd showDelay showBlock showClone enableDelete={!selected} actionHandler={actionHandler} />
|
<BlockActionMenu showClone enableDelete={!selected} actionHandler={actionHandler} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -193,5 +197,8 @@ function TimerIcon(props: { type: TimerType; className: string }) {
|
|||||||
if (type === TimerType.Clock) {
|
if (type === TimerType.Clock) {
|
||||||
return <IoTime className={className} />;
|
return <IoTime className={className} />;
|
||||||
}
|
}
|
||||||
|
if (type === TimerType.TimeToEnd) {
|
||||||
|
return <BiArrowToBottom className={className} />;
|
||||||
|
}
|
||||||
return <IoArrowDown className={className} />;
|
return <IoArrowDown className={className} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ import { tooltipDelayMid } from '../../../../ontimeConfig';
|
|||||||
import { EventItemActions } from '../../RundownEntry';
|
import { EventItemActions } from '../../RundownEntry';
|
||||||
|
|
||||||
interface BlockActionMenuProps {
|
interface BlockActionMenuProps {
|
||||||
showAdd?: boolean;
|
|
||||||
showDelay?: boolean;
|
|
||||||
showBlock?: boolean;
|
|
||||||
enableDelete?: boolean;
|
enableDelete?: boolean;
|
||||||
showClone?: boolean;
|
showClone?: boolean;
|
||||||
actionHandler: (action: EventItemActions, payload?: any) => void;
|
actionHandler: (action: EventItemActions, payload?: any) => void;
|
||||||
@@ -21,7 +18,7 @@ interface BlockActionMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BlockActionMenu(props: BlockActionMenuProps) {
|
export default function BlockActionMenu(props: BlockActionMenuProps) {
|
||||||
const { showAdd, showDelay, showBlock, enableDelete, showClone, actionHandler, className } = props;
|
const { enableDelete, showClone, actionHandler, className } = props;
|
||||||
|
|
||||||
const handleAddEvent = useCallback(() => actionHandler('event'), [actionHandler]);
|
const handleAddEvent = useCallback(() => actionHandler('event'), [actionHandler]);
|
||||||
const handleAddDelay = useCallback(() => actionHandler('delay'), [actionHandler]);
|
const handleAddDelay = useCallback(() => actionHandler('delay'), [actionHandler]);
|
||||||
@@ -43,17 +40,17 @@ export default function BlockActionMenu(props: BlockActionMenuProps) {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem icon={<IoAdd />} onClick={handleAddEvent} isDisabled={!showAdd}>
|
<MenuItem icon={<IoAdd />} onClick={handleAddEvent}>
|
||||||
Add Event after
|
Add Event after
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<IoTimerOutline />} onClick={handleAddDelay} isDisabled={!showDelay}>
|
<MenuItem icon={<IoTimerOutline />} onClick={handleAddDelay}>
|
||||||
Add Delay after
|
Add Delay after
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<IoRemoveCircleOutline />} onClick={handleAddBlock} isDisabled={!showBlock}>
|
<MenuItem icon={<IoRemoveCircleOutline />} onClick={handleAddBlock}>
|
||||||
Add Block after
|
Add Block after
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{showClone && (
|
{showClone && (
|
||||||
<MenuItem icon={<IoDuplicateOutline />} onClick={handleClone} isDisabled={!showBlock}>
|
<MenuItem icon={<IoDuplicateOutline />} onClick={handleClone}>
|
||||||
Clone event
|
Clone event
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,26 +4,27 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 4px 0;
|
margin: 0.25rem 0;
|
||||||
font-size: 12px;
|
font-size: $inner-section-text-size;
|
||||||
padding: 0 10px;
|
padding: 0 0.75rem;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btnRow {
|
.btnRow {
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10%;
|
gap: max(0.5rem, 2rem);
|
||||||
|
|
||||||
.quickBtn {
|
.quickBtn {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 0 32px;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyboard {
|
.keyboard {
|
||||||
margin-left: 8px;
|
margin-left: 0.5rem;
|
||||||
padding: 0 4px;
|
padding: 0 0.25rem;
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
@@ -32,4 +33,5 @@
|
|||||||
.options {
|
.options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
|||||||
size='xs'
|
size='xs'
|
||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
className={style.quickBtn}
|
className={style.quickBtn}
|
||||||
|
data-testid='quick-add-event'
|
||||||
>
|
>
|
||||||
Event {showKbd && <span className={style.keyboard}>Alt + E</span>}
|
Event {showKbd && <span className={style.keyboard}>Alt + E</span>}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -91,6 +92,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
|||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
disabled={disableAddDelay}
|
disabled={disableAddDelay}
|
||||||
className={style.quickBtn}
|
className={style.quickBtn}
|
||||||
|
data-testid='quick-add-delay'
|
||||||
>
|
>
|
||||||
Delay {showKbd && <span className={style.keyboard}>Alt + D</span>}
|
Delay {showKbd && <span className={style.keyboard}>Alt + D</span>}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -102,6 +104,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
|||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
disabled={disableAddBlock}
|
disabled={disableAddBlock}
|
||||||
className={style.quickBtn}
|
className={style.quickBtn}
|
||||||
|
data-testid='quick-add-block'
|
||||||
>
|
>
|
||||||
Block {showKbd && <span className={style.keyboard}>Alt + B</span>}
|
Block {showKbd && <span className={style.keyboard}>Alt + B</span>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface EventIdSwappingStore {
|
||||||
|
selectedEventId: string | null;
|
||||||
|
setSelectedEventId: (newEventId: string | null) => void;
|
||||||
|
clearSelectedEventId: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEventIdSwapping = create<EventIdSwappingStore>((set) => ({
|
||||||
|
selectedEventId: null,
|
||||||
|
setSelectedEventId: (newEventId) => set(() => ({ selectedEventId: newEventId })),
|
||||||
|
clearSelectedEventId: () => set(() => ({ selectedEventId: null })),
|
||||||
|
}));
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
import { EventData, Message, OntimeEvent, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
import { formatDisplay } from 'ontime-utils';
|
import { formatDisplay } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
@@ -15,7 +15,6 @@ import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-edito
|
|||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
import { titleVariants } from '../common/animation';
|
import { titleVariants } from '../common/animation';
|
||||||
@@ -74,7 +73,7 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
: formatTime(time.expectedFinish, formatOptions);
|
: formatTime(time.expectedFinish, formatOptions);
|
||||||
|
|
||||||
const qrSize = Math.max(window.innerWidth / 15, 128);
|
const qrSize = Math.max(window.innerWidth / 15, 128);
|
||||||
const filteredEvents = getEventsWithDelay(backstageEvents);
|
const filteredEvents = backstageEvents.filter((event) => event.type === SupportedEvent.Event);
|
||||||
const showPublicMessage = publ.text && publ.visible;
|
const showPublicMessage = publ.text && publ.visible;
|
||||||
const showProgress = time.playback !== 'stop';
|
const showProgress = time.playback !== 'stop';
|
||||||
|
|
||||||
@@ -167,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'}>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function getTimerByType(timerObject?: TimerTypeParams): string | number |
|
|||||||
return timer;
|
return timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timerObject.timerType === TimerType.CountDown) {
|
if (timerObject.timerType === TimerType.CountDown || timerObject.timerType === TimerType.TimeToEnd) {
|
||||||
timer = timerObject.current;
|
timer = timerObject.current;
|
||||||
} else if (timerObject.timerType === TimerType.CountUp) {
|
} else if (timerObject.timerType === TimerType.CountUp) {
|
||||||
timer = timerObject.elapsed;
|
timer = timerObject.elapsed;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-edito
|
|||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import getDelayTo from '../../../common/utils/getDelayTo';
|
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
if (followThis !== null) {
|
if (followThis !== null) {
|
||||||
setFollow(followThis);
|
setFollow(followThis);
|
||||||
const idx: number = backstageEvents.findIndex((event: OntimeRundownEntry) => event.id === followThis?.id);
|
const idx: number = backstageEvents.findIndex((event: OntimeRundownEntry) => event.id === followThis?.id);
|
||||||
const delayToEvent = getDelayTo(backstageEvents, idx);
|
const delayToEvent = backstageEvents[idx]?.delay ?? 0;
|
||||||
setDelay(delayToEvent);
|
setDelay(delayToEvent);
|
||||||
}
|
}
|
||||||
}, [backstageEvents, searchParams]);
|
}, [backstageEvents, searchParams]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import type { OntimeRundown, ViewSettings } from 'ontime-types';
|
import type { OntimeEvent, OntimeRundown, ViewSettings } from 'ontime-types';
|
||||||
|
import { SupportedEvent } from 'ontime-types';
|
||||||
import { formatDisplay } from 'ontime-utils';
|
import { formatDisplay } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
@@ -11,15 +12,11 @@ import useFitText from '../../../common/hooks/useFitText';
|
|||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { secondsInMillis } from '../../../common/utils/dateConfig';
|
import { secondsInMillis } from '../../../common/utils/dateConfig';
|
||||||
import {
|
|
||||||
formatEventList,
|
|
||||||
getEventsWithDelay,
|
|
||||||
type ScheduleEvent,
|
|
||||||
trimRundown,
|
|
||||||
} from '../../../common/utils/eventsManager';
|
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { TitleManager } from '../ViewWrapper';
|
import { TitleManager } from '../ViewWrapper';
|
||||||
|
|
||||||
|
import { type ScheduleEvent, formatEventList, trimRundown } from './studioClock.utils';
|
||||||
|
|
||||||
import './StudioClock.scss';
|
import './StudioClock.scss';
|
||||||
|
|
||||||
const formatOptions = {
|
const formatOptions = {
|
||||||
@@ -66,8 +63,8 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const delayed = getEventsWithDelay(backstageEvents);
|
const delayed = backstageEvents.filter((event) => event.type === SupportedEvent.Event);
|
||||||
const trimmed = trimRundown(delayed, selectedId || '', MAX_TITLES);
|
const trimmed = trimRundown(delayed as OntimeEvent[], selectedId || '', MAX_TITLES);
|
||||||
|
|
||||||
const formatted = formatEventList(trimmed, selectedId || '', nextId || '', {
|
const formatted = formatEventList(trimmed, selectedId || '', nextId || '', {
|
||||||
showEnd: false,
|
showEnd: false,
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { formatEventList, trimRundown } from '../studioClock.utils';
|
||||||
|
|
||||||
|
describe('test trimEventlist function', () => {
|
||||||
|
const limit = 8;
|
||||||
|
const testData = [
|
||||||
|
{ id: '1' },
|
||||||
|
{ id: '2' },
|
||||||
|
{ id: '3' },
|
||||||
|
{ id: '4' },
|
||||||
|
{ id: '5' },
|
||||||
|
{ id: '6' },
|
||||||
|
{ id: '7' },
|
||||||
|
{ id: '8' },
|
||||||
|
{ id: '9' },
|
||||||
|
{ id: '10' },
|
||||||
|
{ id: '11' },
|
||||||
|
{ id: '12' },
|
||||||
|
];
|
||||||
|
|
||||||
|
it('when we use the first item', () => {
|
||||||
|
const selectedId = '1';
|
||||||
|
const expected = [
|
||||||
|
{ id: '1' },
|
||||||
|
{ id: '2' },
|
||||||
|
{ id: '3' },
|
||||||
|
{ id: '4' },
|
||||||
|
{ id: '5' },
|
||||||
|
{ id: '6' },
|
||||||
|
{ id: '7' },
|
||||||
|
{ id: '8' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||||
|
expect(l.length).toBe(limit);
|
||||||
|
expect(l).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when we use the third item', () => {
|
||||||
|
const selectedId = '3';
|
||||||
|
const expected = [
|
||||||
|
{ id: '1' },
|
||||||
|
{ id: '2' },
|
||||||
|
{ id: '3' },
|
||||||
|
{ id: '4' },
|
||||||
|
{ id: '5' },
|
||||||
|
{ id: '6' },
|
||||||
|
{ id: '7' },
|
||||||
|
{ id: '8' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||||
|
expect(l.length).toBe(limit);
|
||||||
|
expect(l).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when we use the fourth item', () => {
|
||||||
|
const selectedId = '4';
|
||||||
|
const expected = [
|
||||||
|
{ id: '2' },
|
||||||
|
{ id: '3' },
|
||||||
|
{ id: '4' },
|
||||||
|
{ id: '5' },
|
||||||
|
{ id: '6' },
|
||||||
|
{ id: '7' },
|
||||||
|
{ id: '8' },
|
||||||
|
{ id: '9' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||||
|
expect(l.length).toBe(limit);
|
||||||
|
expect(l).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if selected is not found', () => {
|
||||||
|
const selectedId = '15';
|
||||||
|
const expected = [
|
||||||
|
{ id: '1' },
|
||||||
|
{ id: '2' },
|
||||||
|
{ id: '3' },
|
||||||
|
{ id: '4' },
|
||||||
|
{ id: '5' },
|
||||||
|
{ id: '6' },
|
||||||
|
{ id: '7' },
|
||||||
|
{ id: '8' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const l = trimRundown(testData as OntimeEvent[], selectedId, limit);
|
||||||
|
expect(l.length).toBe(limit);
|
||||||
|
expect(l).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('test formatEvents function', () => {
|
||||||
|
const testEvent = [
|
||||||
|
{
|
||||||
|
title: 'Welcome to Ontime',
|
||||||
|
subtitle: 'Subtitles are useful',
|
||||||
|
presenter: 'cpvalente',
|
||||||
|
note: 'Maybe a running note for the operator?',
|
||||||
|
timeStart: 28800000,
|
||||||
|
timeEnd: 30600000,
|
||||||
|
isPublic: false,
|
||||||
|
colour: '',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
revision: 0,
|
||||||
|
id: '5946',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Unless recalled by the OSC address',
|
||||||
|
subtitle: '',
|
||||||
|
presenter: '',
|
||||||
|
note: 'In green, below',
|
||||||
|
timeStart: 34800000,
|
||||||
|
timeEnd: 35400000,
|
||||||
|
isPublic: false,
|
||||||
|
colour: '',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
revision: 0,
|
||||||
|
id: '8ee5',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('it parses correctly', () => {
|
||||||
|
const selectedId = 'otherEvent';
|
||||||
|
const nextId = 'notHere';
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: '5946',
|
||||||
|
time: '08:00 - 08:30',
|
||||||
|
title: 'Welcome to Ontime',
|
||||||
|
isNow: false,
|
||||||
|
isNext: false,
|
||||||
|
colour: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8ee5',
|
||||||
|
time: '09:40 - 09:50',
|
||||||
|
title: 'Unless recalled by the OSC address',
|
||||||
|
isNow: false,
|
||||||
|
isNext: false,
|
||||||
|
colour: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||||
|
expect(parsed).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it handles selected correctly', () => {
|
||||||
|
const selectedId = '5946';
|
||||||
|
const nextId = '8ee5';
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: '5946',
|
||||||
|
time: '08:00 - 08:30',
|
||||||
|
title: 'Welcome to Ontime',
|
||||||
|
isNow: true,
|
||||||
|
isNext: false,
|
||||||
|
colour: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8ee5',
|
||||||
|
time: '09:40 - 09:50',
|
||||||
|
title: 'Unless recalled by the OSC address',
|
||||||
|
isNow: false,
|
||||||
|
isNext: true,
|
||||||
|
colour: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||||
|
expect(parsed).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it handles next correctly', () => {
|
||||||
|
const selectedId = '8ee5';
|
||||||
|
const nextId = 'notHere';
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: '5946',
|
||||||
|
time: '08:00 - 08:30',
|
||||||
|
title: 'Welcome to Ontime',
|
||||||
|
isNow: false,
|
||||||
|
isNext: false,
|
||||||
|
colour: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8ee5',
|
||||||
|
time: '09:40 - 09:50',
|
||||||
|
title: 'Unless recalled by the OSC address',
|
||||||
|
isNow: true,
|
||||||
|
isNext: false,
|
||||||
|
colour: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const parsed = formatEventList(testEvent as OntimeEvent[], selectedId, nextId, { showEnd: true });
|
||||||
|
expect(parsed).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
|
export type ScheduleEvent = {
|
||||||
|
id: string;
|
||||||
|
time: string;
|
||||||
|
title: string;
|
||||||
|
isNow: boolean;
|
||||||
|
isNext: boolean;
|
||||||
|
colour: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns trimmed event list array
|
||||||
|
* @param {Object[]} rundown - given rundown
|
||||||
|
* @param {string} selectedId - id of currently selected event
|
||||||
|
* @param {number} limit - max number of events to return
|
||||||
|
* @returns {Object[]} Event list with maximum <limit> objects
|
||||||
|
*/
|
||||||
|
export const trimRundown = (rundown: OntimeEvent[], selectedId: string, limit: number): OntimeEvent[] => {
|
||||||
|
if (rundown == null) return [];
|
||||||
|
|
||||||
|
const BEFORE = 2;
|
||||||
|
const trimmedRundown = [...rundown];
|
||||||
|
|
||||||
|
// limit events length if necessary
|
||||||
|
if (limit != null) {
|
||||||
|
while (trimmedRundown.length > limit) {
|
||||||
|
const idx = trimmedRundown.findIndex((e) => e.id === selectedId);
|
||||||
|
if (idx <= BEFORE) {
|
||||||
|
trimmedRundown.pop();
|
||||||
|
} else {
|
||||||
|
trimmedRundown.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmedRundown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormatEventListOptionsProp = {
|
||||||
|
showEnd?: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @description Returns list of events formatted to be displayed
|
||||||
|
* @param {Object[]} rundown - given rundown
|
||||||
|
* @param {string} selectedId - id of currently selected event
|
||||||
|
* @param {string} nextId - id of next event
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {boolean} [options.showEnd] - whether to show the end time
|
||||||
|
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
|
||||||
|
*/
|
||||||
|
export const formatEventList = (
|
||||||
|
rundown: OntimeEvent[],
|
||||||
|
selectedId: string,
|
||||||
|
nextId: string,
|
||||||
|
options: FormatEventListOptionsProp,
|
||||||
|
): ScheduleEvent[] => {
|
||||||
|
if (rundown == null) return [];
|
||||||
|
const { showEnd = false } = options;
|
||||||
|
|
||||||
|
// format list
|
||||||
|
return rundown.map((event) => {
|
||||||
|
const start = formatTime(event.timeStart + (event.delay || 0));
|
||||||
|
const end = formatTime(event.timeEnd + (event.delay || 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: event.id,
|
||||||
|
time: showEnd ? `${start} - ${end}` : start,
|
||||||
|
title: event.title,
|
||||||
|
isNow: event.id === selectedId,
|
||||||
|
isNext: event.id === nextId,
|
||||||
|
colour: event.colour,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -15,10 +15,10 @@ const root = createRoot(container as Element);
|
|||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: 'https://5e4d2c4b57ab409cb98d4c08b2014755@o4504288369836032.ingest.sentry.io/4504288371343360',
|
dsn: 'https://5e4d2c4b57ab409cb98d4c08b2014755@o4504288369836032.ingest.sentry.io/4504288371343360',
|
||||||
integrations: [new BrowserTracing()],
|
integrations: [new BrowserTracing()],
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 0.3,
|
||||||
release: ONTIME_VERSION,
|
release: ONTIME_VERSION,
|
||||||
enabled: import.meta.env.PROD,
|
enabled: import.meta.env.PROD,
|
||||||
ignoreErrors: ['top.GLOBALS', 'Unable to preload CSS'],
|
ignoreErrors: ['top.GLOBALS', 'Unable to preload CSS', 'Failed to fetch dynamically imported module'],
|
||||||
denyUrls: [/extensions\//i, /^chrome:\/\//i, /^chrome-extension:\/\//i],
|
denyUrls: [/extensions\//i, /^chrome:\/\//i, /^chrome-extension:\/\//i],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ module.exports = {
|
|||||||
shutdownCode: 99,
|
shutdownCode: 99,
|
||||||
},
|
},
|
||||||
reactAppUrl: {
|
reactAppUrl: {
|
||||||
development: 'http://localhost:3000/editor',
|
development: (port = 4001) => `http://localhost:${port}/editor`,
|
||||||
production: 'http://localhost:4001/editor',
|
production: (port = 4001) => `http://localhost:${port}/editor`,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
pathToEntrypoint: '../extraResources/server/index.cjs'
|
pathToEntrypoint: '../extraResources/server/index.cjs',
|
||||||
},
|
},
|
||||||
assets: {
|
assets: {
|
||||||
pathToAssets: './assets/',
|
pathToAssets: './assets/',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-14
@@ -30,21 +30,23 @@ let splash;
|
|||||||
let tray = null;
|
let tray = null;
|
||||||
|
|
||||||
async function startBackend() {
|
async function startBackend() {
|
||||||
await (async () => {
|
// in dev mode, we expect both UI and server to be running
|
||||||
// in dev mode, we expect both UI and server to be running
|
if (!isProduction) {
|
||||||
if (!isProduction) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const ontimeServer = require(nodePath);
|
const ontimeServer = require(nodePath);
|
||||||
const { initAssets, startServer, startOSCServer, startIntegrations } = ontimeServer;
|
const { initAssets, startServer, startOSCServer, startIntegrations } = ontimeServer;
|
||||||
|
|
||||||
await initAssets();
|
await initAssets();
|
||||||
|
|
||||||
loaded = await startServer();
|
const result = await startServer();
|
||||||
await startOSCServer();
|
loaded = result.message;
|
||||||
await startIntegrations();
|
|
||||||
})();
|
await startOSCServer();
|
||||||
|
await startIntegrations();
|
||||||
|
|
||||||
|
return result.serverPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,9 +157,11 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
startBackend()
|
startBackend()
|
||||||
.then(() => {
|
.then((port) => {
|
||||||
// Load page served by node or use React dev run
|
// Load page served by node or use React dev run
|
||||||
const clientUrl = isProduction ? electronConfig.reactAppUrl.production : electronConfig.reactAppUrl.development;
|
const clientUrl = isProduction
|
||||||
|
? electronConfig.reactAppUrl.production(port)
|
||||||
|
: electronConfig.reactAppUrl.development(port);
|
||||||
|
|
||||||
win
|
win
|
||||||
.loadURL(clientUrl)
|
.loadURL(clientUrl)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.3.9",
|
"version": "2.7.2",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "2.3.9",
|
"version": "2.7.2",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.20.0",
|
"body-parser": "^1.20.0",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const initAssets = async () => {
|
|||||||
export const startServer = async () => {
|
export const startServer = async () => {
|
||||||
checkStart(OntimeStartOrder.InitServer);
|
checkStart(OntimeStartOrder.InitServer);
|
||||||
|
|
||||||
const serverPort = 4001; // hardcoded for now
|
const { serverPort } = DataProvider.getSettings();
|
||||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||||
|
|
||||||
expressServer = http.createServer(app);
|
expressServer = http.createServer(app);
|
||||||
@@ -144,7 +144,7 @@ export const startServer = async () => {
|
|||||||
|
|
||||||
expressServer.listen(serverPort, '0.0.0.0');
|
expressServer.listen(serverPort, '0.0.0.0');
|
||||||
|
|
||||||
return returnMessage;
|
return { message: returnMessage, serverPort };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Class Event Provider is a mediator for handling the local db
|
* Class Event Provider is a mediator for handling the local db
|
||||||
* and adds logic specific to ontime data
|
* and adds logic specific to ontime data
|
||||||
*/
|
*/
|
||||||
import { EventData, ViewSettings } from 'ontime-types';
|
import { EventData, OntimeRundown, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { data, db } from '../../modules/loadDb.js';
|
import { data, db } from '../../modules/loadDb.js';
|
||||||
import { safeMerge } from './DataProvider.utils.js';
|
import { safeMerge } from './DataProvider.utils.js';
|
||||||
@@ -22,19 +22,15 @@ export class DataProvider {
|
|||||||
return data.eventData;
|
return data.eventData;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setRundown(newData) {
|
static async setRundown(newData: OntimeRundown) {
|
||||||
data.rundown = [...newData];
|
data.rundown = [...newData];
|
||||||
await this.persist();
|
await this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getIndexOf(eventId) {
|
static getIndexOf(eventId: string) {
|
||||||
return data.rundown.findIndex((e) => e.id === eventId);
|
return data.rundown.findIndex((e) => e.id === eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getEventById(eventId) {
|
|
||||||
return data.rundown.find((e) => e.id === eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getRundownLength() {
|
static getRundownLength() {
|
||||||
return data.rundown.length;
|
return data.rundown.length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ export class EventLoader {
|
|||||||
return timedEvents.find((event) => event.id === eventId);
|
return timedEvents.find((event) => event.id === eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns first event given its cue
|
||||||
|
* @param {string} cue
|
||||||
|
* @return {object | undefined}
|
||||||
|
*/
|
||||||
|
static getEventWithCue(cue) {
|
||||||
|
const timedEvents = EventLoader.getTimedEvents();
|
||||||
|
return timedEvents.find((event) => event.cue === cue);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* loads an event given its id
|
* loads an event given its id
|
||||||
* @param {string} eventId
|
* @param {string} eventId
|
||||||
|
|||||||
@@ -123,6 +123,15 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
|
|||||||
PlaybackService.startById(payload);
|
PlaybackService.startById(payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'startcue': {
|
||||||
|
if (!payload || typeof payload !== 'string') {
|
||||||
|
throw new Error(`Event cue not recognised: ${payload}`);
|
||||||
|
}
|
||||||
|
PlaybackService.startByCue(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'pause': {
|
case 'pause': {
|
||||||
PlaybackService.pause();
|
PlaybackService.pause();
|
||||||
break;
|
break;
|
||||||
@@ -189,6 +198,19 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'gotocue':
|
||||||
|
case 'loadcue': {
|
||||||
|
if (!payload || typeof payload !== 'string') {
|
||||||
|
throw new Error(`Event cue not recognised: ${payload}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PlaybackService.loadByCue(payload);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`OSC IN: error calling goto ${error}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'get-playback': {
|
case 'get-playback': {
|
||||||
const playback = eventStore.get('playback');
|
const playback = eventStore.get('playback');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
|||||||
import { mergeObject } from '../utils/parserUtils.js';
|
import { mergeObject } from '../utils/parserUtils.js';
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
import { PlaybackService } from '../services/PlaybackService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { resolveDbPath } from '../setup.js';
|
import { isDocker, resolveDbPath } from '../setup.js';
|
||||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
import { deleteAllEvents, forceReset } from '../services/rundown-service/RundownService.js';
|
import { deleteAllEvents, forceReset } from '../services/rundown-service/RundownService.js';
|
||||||
@@ -208,6 +208,11 @@ export const postSettings = async (req, res) => {
|
|||||||
const editorKey = extractPin(req.body?.editorKey, settings.editorKey);
|
const editorKey = extractPin(req.body?.editorKey, settings.editorKey);
|
||||||
const operatorKey = extractPin(req.body?.operatorKey, settings.operatorKey);
|
const operatorKey = extractPin(req.body?.operatorKey, settings.operatorKey);
|
||||||
|
|
||||||
|
if (isDocker && req.body?.serverPort) {
|
||||||
|
return res.status(403).json({ message: `Can't change port when running inside docker` });
|
||||||
|
}
|
||||||
|
const serverPort = parseInt(req.body?.serverPort ?? settings.serverPort, 10);
|
||||||
|
|
||||||
let timeFormat = settings.timeFormat;
|
let timeFormat = settings.timeFormat;
|
||||||
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
|
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
|
||||||
timeFormat = req.body.timeFormat;
|
timeFormat = req.body.timeFormat;
|
||||||
@@ -221,6 +226,7 @@ export const postSettings = async (req, res) => {
|
|||||||
operatorKey,
|
operatorKey,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
language,
|
language,
|
||||||
|
serverPort,
|
||||||
};
|
};
|
||||||
await DataProvider.setSettings(newData);
|
await DataProvider.setSettings(newData);
|
||||||
res.status(200).send(newData);
|
res.status(200).send(newData);
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const validateSettings = [
|
|||||||
body('operatorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
|
body('operatorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
|
||||||
body('timeFormat').isString().isIn(['12', '24']),
|
body('timeFormat').isString().isIn(['12', '24']),
|
||||||
body('language').isString(),
|
body('language').isString(),
|
||||||
|
body('serverPort').isPort().optional(),
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { OntimeEvent } from 'ontime-types';
|
|
||||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||||
import {
|
import {
|
||||||
addEvent,
|
addEvent,
|
||||||
@@ -7,19 +6,21 @@ import {
|
|||||||
deleteEvent,
|
deleteEvent,
|
||||||
editEvent,
|
editEvent,
|
||||||
reorderEvent,
|
reorderEvent,
|
||||||
|
swapEvents,
|
||||||
} from '../services/rundown-service/RundownService.js';
|
} from '../services/rundown-service/RundownService.js';
|
||||||
import { getDelayedRundown } from '../services/rundown-service/delayedRundown.utils.js';
|
import { getDelayedRundown } from '../services/rundown-service/delayedRundown.utils.js';
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
|
||||||
// Create controller for GET request to '/events'
|
// Create controller for GET request to '/events'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownGetAll = async (req, res) => {
|
export const rundownGetAll: RequestHandler = async (_req, res) => {
|
||||||
const delayedRundown = getDelayedRundown();
|
const delayedRundown = getDelayedRundown();
|
||||||
res.json(delayedRundown);
|
res.json(delayedRundown);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create controller for POST request to '/events/'
|
// Create controller for POST request to '/events/'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownPost = async (req, res) => {
|
export const rundownPost: RequestHandler = async (req, res) => {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -34,7 +35,7 @@ export const rundownPost = async (req, res) => {
|
|||||||
|
|
||||||
// Create controller for PUT request to '/events/'
|
// Create controller for PUT request to '/events/'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownPut = async (req, res) => {
|
export const rundownPut: RequestHandler = async (req, res) => {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,7 @@ export const rundownPut = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rundownReorder = async (req, res) => {
|
export const rundownReorder: RequestHandler = async (req, res) => {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -61,9 +62,23 @@ export const rundownReorder = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const rundownSwap: RequestHandler = async (req, res) => {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { from, to } = req.body;
|
||||||
|
await swapEvents(from, to);
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Create controller for PATCH request to '/events/applydelay/:eventId'
|
// Create controller for PATCH request to '/events/applydelay/:eventId'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownApplyDelay = async (req, res) => {
|
export const rundownApplyDelay: RequestHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await applyDelay(req.params.eventId);
|
await applyDelay(req.params.eventId);
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
@@ -74,7 +89,7 @@ export const rundownApplyDelay = async (req, res) => {
|
|||||||
|
|
||||||
// Create controller for DELETE request to '/events/:eventId'
|
// Create controller for DELETE request to '/events/:eventId'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const deleteEventById = async (req, res) => {
|
export const deleteEventById: RequestHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await deleteEvent(req.params.eventId);
|
await deleteEvent(req.params.eventId);
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
@@ -85,7 +100,7 @@ export const deleteEventById = async (req, res) => {
|
|||||||
|
|
||||||
// Create controller for DELETE request to '/events/'
|
// Create controller for DELETE request to '/events/'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownDelete = async (req, res) => {
|
export const rundownDelete: RequestHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await deleteAllEvents();
|
await deleteAllEvents();
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ export const rundownReorderValidator = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const rundownSwapValidator = [
|
||||||
|
body('from').isString().exists(),
|
||||||
|
body('to').isString().exists(),
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const paramsMustHaveEventId = [
|
export const paramsMustHaveEventId = [
|
||||||
param('eventId').exists(),
|
param('eventId').exists(),
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EndAction, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
import { EndAction, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
export const event: Omit<OntimeEvent, 'id' | 'delay'> = {
|
export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||||
title: '',
|
title: '',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
presenter: '',
|
presenter: '',
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
rundownPost,
|
rundownPost,
|
||||||
rundownPut,
|
rundownPut,
|
||||||
rundownReorder,
|
rundownReorder,
|
||||||
|
rundownSwap,
|
||||||
} from '../controllers/rundownController.js';
|
} from '../controllers/rundownController.js';
|
||||||
import {
|
import {
|
||||||
paramsMustHaveEventId,
|
paramsMustHaveEventId,
|
||||||
rundownPostValidator,
|
rundownPostValidator,
|
||||||
rundownPutValidator,
|
rundownPutValidator,
|
||||||
rundownReorderValidator,
|
rundownReorderValidator,
|
||||||
|
rundownSwapValidator,
|
||||||
} from '../controllers/rundownController.validate.js';
|
} from '../controllers/rundownController.validate.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
@@ -29,6 +31,8 @@ router.put('/', rundownPutValidator, rundownPut);
|
|||||||
// create route between controller and '/events/reorder' endpoint
|
// create route between controller and '/events/reorder' endpoint
|
||||||
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
||||||
|
|
||||||
|
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
||||||
|
|
||||||
// create route between controller and '/events/applydelay/:eventId' endpoint
|
// create route between controller and '/events/applydelay/:eventId' endpoint
|
||||||
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,21 @@ export class PlaybackService {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts first event matching given cue
|
||||||
|
* @param {string} cue
|
||||||
|
* @return {boolean} success
|
||||||
|
*/
|
||||||
|
static startByCue(cue: string): boolean {
|
||||||
|
const event = EventLoader.getEventWithCue(cue);
|
||||||
|
const success = PlaybackService.loadEvent(event);
|
||||||
|
if (success) {
|
||||||
|
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||||
|
PlaybackService.start();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* loads event matching given ID
|
* loads event matching given ID
|
||||||
* @param {string} eventId
|
* @param {string} eventId
|
||||||
@@ -90,6 +105,20 @@ export class PlaybackService {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads first event matching given cue
|
||||||
|
* @param {string} cue
|
||||||
|
* @return {boolean} success
|
||||||
|
*/
|
||||||
|
static loadByCue(cue: string): boolean {
|
||||||
|
const event = EventLoader.getEventWithCue(cue);
|
||||||
|
const success = PlaybackService.loadEvent(event);
|
||||||
|
if (success) {
|
||||||
|
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads event before currently selected
|
* Loads event before currently selected
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types';
|
import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState, TimerType } from 'ontime-types';
|
||||||
import { calculateDuration, dayInMs } from 'ontime-utils';
|
import { calculateDuration, dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
@@ -24,6 +24,9 @@ export class TimerService {
|
|||||||
timer: TimerState;
|
timer: TimerState;
|
||||||
|
|
||||||
loadedTimerId: string | null;
|
loadedTimerId: string | null;
|
||||||
|
private loadedTimerStart: number | null;
|
||||||
|
private loadedTimerEnd: number | null;
|
||||||
|
|
||||||
private pausedTime: number;
|
private pausedTime: number;
|
||||||
private pausedAt: number | null;
|
private pausedAt: number | null;
|
||||||
private secondaryTarget: number | null;
|
private secondaryTarget: number | null;
|
||||||
@@ -61,6 +64,9 @@ export class TimerService {
|
|||||||
endAction: null,
|
endAction: null,
|
||||||
};
|
};
|
||||||
this.loadedTimerId = null;
|
this.loadedTimerId = null;
|
||||||
|
this.loadedTimerStart = null;
|
||||||
|
this.loadedTimerEnd = null;
|
||||||
|
|
||||||
this.pausedTime = 0;
|
this.pausedTime = 0;
|
||||||
this.pausedAt = null;
|
this.pausedAt = null;
|
||||||
this.secondaryTarget = null;
|
this.secondaryTarget = null;
|
||||||
@@ -93,6 +99,8 @@ export class TimerService {
|
|||||||
this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
|
this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
|
||||||
this.timer.timerType = timer.timerType;
|
this.timer.timerType = timer.timerType;
|
||||||
this.timer.endAction = timer.endAction;
|
this.timer.endAction = timer.endAction;
|
||||||
|
this.loadedTimerStart = timer.timeStart;
|
||||||
|
this.loadedTimerEnd = timer.timeEnd;
|
||||||
|
|
||||||
// this might not be ideal
|
// this might not be ideal
|
||||||
this.timer.finishedAt = null;
|
this.timer.finishedAt = null;
|
||||||
@@ -102,6 +110,8 @@ export class TimerService {
|
|||||||
this.timer.duration,
|
this.timer.duration,
|
||||||
this.pausedTime,
|
this.pausedTime,
|
||||||
this.timer.addedTime,
|
this.timer.addedTime,
|
||||||
|
this.loadedTimerEnd,
|
||||||
|
this.timer.timerType,
|
||||||
);
|
);
|
||||||
if (this.timer.startedAt === null) {
|
if (this.timer.startedAt === null) {
|
||||||
this.timer.current = this.timer.duration;
|
this.timer.current = this.timer.duration;
|
||||||
@@ -129,14 +139,22 @@ export class TimerService {
|
|||||||
this._clear();
|
this._clear();
|
||||||
|
|
||||||
this.loadedTimerId = timer.id;
|
this.loadedTimerId = timer.id;
|
||||||
|
this.loadedTimerStart = timer.timeStart;
|
||||||
|
this.loadedTimerEnd = timer.timeEnd;
|
||||||
|
|
||||||
this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
|
this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
|
||||||
this.timer.current = this.timer.duration;
|
|
||||||
this.playback = Playback.Armed;
|
this.playback = Playback.Armed;
|
||||||
this.timer.timerType = timer.timerType;
|
this.timer.timerType = timer.timerType;
|
||||||
this.timer.endAction = timer.endAction;
|
this.timer.endAction = timer.endAction;
|
||||||
this.pausedTime = 0;
|
this.pausedTime = 0;
|
||||||
this.pausedAt = 0;
|
this.pausedAt = 0;
|
||||||
|
|
||||||
|
this.timer.current = this.timer.duration;
|
||||||
|
if (this.timer.timerType === TimerType.TimeToEnd) {
|
||||||
|
const now = clock.timeNow();
|
||||||
|
this.timer.current = getCurrent(now, this.timer.duration, 0, 0, now, timer.timeEnd, this.timer.timerType);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof initialData !== 'undefined') {
|
if (typeof initialData !== 'undefined') {
|
||||||
this.timer = { ...this.timer, ...initialData };
|
this.timer = { ...this.timer, ...initialData };
|
||||||
}
|
}
|
||||||
@@ -188,6 +206,8 @@ export class TimerService {
|
|||||||
this.timer.duration,
|
this.timer.duration,
|
||||||
this.pausedTime,
|
this.pausedTime,
|
||||||
this.timer.addedTime,
|
this.timer.addedTime,
|
||||||
|
this.loadedTimerEnd,
|
||||||
|
this.timer.timerType,
|
||||||
);
|
);
|
||||||
this._onStart();
|
this._onStart();
|
||||||
}
|
}
|
||||||
@@ -310,6 +330,8 @@ export class TimerService {
|
|||||||
this.timer.duration,
|
this.timer.duration,
|
||||||
this.pausedTime,
|
this.pausedTime,
|
||||||
this.timer.addedTime,
|
this.timer.addedTime,
|
||||||
|
this.loadedTimerEnd,
|
||||||
|
this.timer.timerType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.timer.current = getCurrent(
|
this.timer.current = getCurrent(
|
||||||
@@ -318,6 +340,8 @@ export class TimerService {
|
|||||||
this.timer.addedTime,
|
this.timer.addedTime,
|
||||||
this.pausedTime,
|
this.pausedTime,
|
||||||
this.timer.clock,
|
this.timer.clock,
|
||||||
|
this.loadedTimerEnd,
|
||||||
|
this.timer.timerType,
|
||||||
);
|
);
|
||||||
this.timer.elapsed = this.timer.duration - this.timer.current;
|
this.timer.elapsed = this.timer.duration - this.timer.current;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { dayInMs } from 'ontime-utils';
|
import { dayInMs } from 'ontime-utils';
|
||||||
|
import { TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { getCurrent, getExpectedFinish } from '../timerUtils.js';
|
import { getCurrent, getExpectedFinish } from '../timerUtils.js';
|
||||||
|
|
||||||
@@ -9,7 +10,17 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = 10;
|
const duration = 10;
|
||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 10;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(null);
|
expect(calculatedFinish).toBe(null);
|
||||||
});
|
});
|
||||||
it('is finishedAt if defined', () => {
|
it('is finishedAt if defined', () => {
|
||||||
@@ -18,7 +29,17 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = 10;
|
const duration = 10;
|
||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 20;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(finishedAt);
|
expect(calculatedFinish).toBe(finishedAt);
|
||||||
});
|
});
|
||||||
it('calculates the finish time', () => {
|
it('calculates the finish time', () => {
|
||||||
@@ -27,7 +48,17 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = 10;
|
const duration = 10;
|
||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 11;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(11);
|
expect(calculatedFinish).toBe(11);
|
||||||
});
|
});
|
||||||
it('adds paused and added times', () => {
|
it('adds paused and added times', () => {
|
||||||
@@ -36,7 +67,17 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = 10;
|
const duration = 10;
|
||||||
const pausedTime = 10;
|
const pausedTime = 10;
|
||||||
const addedTime = 10;
|
const addedTime = 10;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 11;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(31);
|
expect(calculatedFinish).toBe(31);
|
||||||
});
|
});
|
||||||
it('added time could be negative', () => {
|
it('added time could be negative', () => {
|
||||||
@@ -45,7 +86,17 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = 10;
|
const duration = 10;
|
||||||
const pausedTime = 10;
|
const pausedTime = 10;
|
||||||
const addedTime = -10;
|
const addedTime = -10;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 11;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(11);
|
expect(calculatedFinish).toBe(11);
|
||||||
});
|
});
|
||||||
it('user could add enough time for it to be negative', () => {
|
it('user could add enough time for it to be negative', () => {
|
||||||
@@ -54,7 +105,17 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = 10;
|
const duration = 10;
|
||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = -100;
|
const addedTime = -100;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 11;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
});
|
});
|
||||||
it('timer can have no duration', () => {
|
it('timer can have no duration', () => {
|
||||||
@@ -63,7 +124,17 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = 0;
|
const duration = 0;
|
||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 0;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
});
|
});
|
||||||
it('finish can be the day after', () => {
|
it('finish can be the day after', () => {
|
||||||
@@ -72,9 +143,60 @@ describe('getExpectedFinish()', () => {
|
|||||||
const duration = dayInMs;
|
const duration = dayInMs;
|
||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 10;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
expect(calculatedFinish).toBe(10);
|
expect(calculatedFinish).toBe(10);
|
||||||
});
|
});
|
||||||
|
describe('on timers of type time-to-end', () => {
|
||||||
|
it('finish time is as schedule + added time', () => {
|
||||||
|
const startedAt = 10;
|
||||||
|
const finishedAt = null;
|
||||||
|
const duration = dayInMs;
|
||||||
|
const pausedTime = 0;
|
||||||
|
const addedTime = 10;
|
||||||
|
const endTime = 30;
|
||||||
|
const timerType = TimerType.TimeToEnd;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
|
expect(calculatedFinish).toBe(40);
|
||||||
|
});
|
||||||
|
it('handles events that finish the day after', () => {
|
||||||
|
const startedAt = 79200000; // 22:00:00
|
||||||
|
const finishedAt = null;
|
||||||
|
const duration = Infinity; // not relevant
|
||||||
|
const pausedTime = 0;
|
||||||
|
const addedTime = 0;
|
||||||
|
const endTime = 600000; // 00:10:00
|
||||||
|
const timerType = TimerType.TimeToEnd;
|
||||||
|
const calculatedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
|
// expected finish is not a duration but a timetag
|
||||||
|
expect(calculatedFinish).toBe(600000);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCurrent()', () => {
|
describe('getCurrent()', () => {
|
||||||
@@ -84,7 +206,9 @@ describe('getCurrent()', () => {
|
|||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const clock = 0;
|
const clock = 0;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const endTime = 0;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
expect(current).toBe(null);
|
expect(current).toBe(null);
|
||||||
});
|
});
|
||||||
it('is the remaining time in clock', () => {
|
it('is the remaining time in clock', () => {
|
||||||
@@ -93,7 +217,9 @@ describe('getCurrent()', () => {
|
|||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const clock = 1;
|
const clock = 1;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const endTime = 10;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
expect(current).toBe(9);
|
expect(current).toBe(9);
|
||||||
});
|
});
|
||||||
it('accounts for added times', () => {
|
it('accounts for added times', () => {
|
||||||
@@ -102,7 +228,9 @@ describe('getCurrent()', () => {
|
|||||||
const pausedTime = 5;
|
const pausedTime = 5;
|
||||||
const addedTime = 5;
|
const addedTime = 5;
|
||||||
const clock = 1;
|
const clock = 1;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const endTime = 10;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
expect(current).toBe(19);
|
expect(current).toBe(19);
|
||||||
});
|
});
|
||||||
it('counts over midnight', () => {
|
it('counts over midnight', () => {
|
||||||
@@ -111,7 +239,9 @@ describe('getCurrent()', () => {
|
|||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const clock = 10;
|
const clock = 10;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const endTime = 20;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
expect(current).toBe(dayInMs + 10);
|
expect(current).toBe(dayInMs + 10);
|
||||||
});
|
});
|
||||||
it('rolls over midnight', () => {
|
it('rolls over midnight', () => {
|
||||||
@@ -120,7 +250,9 @@ describe('getCurrent()', () => {
|
|||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const clock = 5;
|
const clock = 5;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const endTime = 20;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
expect(current).toBe(15);
|
expect(current).toBe(15);
|
||||||
});
|
});
|
||||||
it('midnight holds delays', () => {
|
it('midnight holds delays', () => {
|
||||||
@@ -129,9 +261,46 @@ describe('getCurrent()', () => {
|
|||||||
const pausedTime = 10;
|
const pausedTime = 10;
|
||||||
const addedTime = 10;
|
const addedTime = 10;
|
||||||
const clock = 5;
|
const clock = 5;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const endTime = 20;
|
||||||
|
const timerType = TimerType.CountDown;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
expect(current).toBe(35);
|
expect(current).toBe(35);
|
||||||
});
|
});
|
||||||
|
describe('on timers of type time-to-end', () => {
|
||||||
|
it('current time is the time to end', () => {
|
||||||
|
const startedAt = 10;
|
||||||
|
const duration = 100;
|
||||||
|
const pausedTime = 0;
|
||||||
|
const addedTime = 0;
|
||||||
|
const clock = 30;
|
||||||
|
const endTime = 100;
|
||||||
|
const timerType = TimerType.TimeToEnd;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
|
expect(current).toBe(70);
|
||||||
|
});
|
||||||
|
it('current time is the time to end + added time', () => {
|
||||||
|
const startedAt = 10;
|
||||||
|
const duration = 100;
|
||||||
|
const pausedTime = 3;
|
||||||
|
const addedTime = 4;
|
||||||
|
const clock = 30;
|
||||||
|
const endTime = 100;
|
||||||
|
const timerType = TimerType.TimeToEnd;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
|
expect(current).toBe(77);
|
||||||
|
});
|
||||||
|
it('handles events that finish the day after', () => {
|
||||||
|
const startedAt = 79200000; // 22:00:00
|
||||||
|
const duration = Infinity; // not relevant
|
||||||
|
const pausedTime = 0;
|
||||||
|
const addedTime = 0;
|
||||||
|
const clock = 79500000; // 22:05:00
|
||||||
|
const endTime = 600000; // 00:10:00
|
||||||
|
const timerType = TimerType.TimeToEnd;
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
|
expect(current).toBe(600000 + dayInMs - clock);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
||||||
@@ -142,8 +311,18 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
|||||||
const pausedTime = 0;
|
const pausedTime = 0;
|
||||||
const addedTime = 0;
|
const addedTime = 0;
|
||||||
const clock = 0;
|
const clock = 0;
|
||||||
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 10;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const timerType = TimerType.CountDown;
|
||||||
|
const expectedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
const elapsed = duration - current;
|
const elapsed = duration - current;
|
||||||
expect(expectedFinish).toBe(10);
|
expect(expectedFinish).toBe(10);
|
||||||
expect(elapsed).toBe(0);
|
expect(elapsed).toBe(0);
|
||||||
@@ -157,8 +336,18 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
|||||||
const pausedTime = 1;
|
const pausedTime = 1;
|
||||||
const addedTime = 2;
|
const addedTime = 2;
|
||||||
const clock = 5;
|
const clock = 5;
|
||||||
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
|
const endTime = 10;
|
||||||
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
|
const timerType = TimerType.CountDown;
|
||||||
|
const expectedFinish = getExpectedFinish(
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
duration,
|
||||||
|
pausedTime,
|
||||||
|
addedTime,
|
||||||
|
endTime,
|
||||||
|
timerType,
|
||||||
|
);
|
||||||
|
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock, endTime, timerType);
|
||||||
const elapsed = duration - current;
|
const elapsed = duration - current;
|
||||||
expect(expectedFinish).toBe(13);
|
expect(expectedFinish).toBe(13);
|
||||||
expect(elapsed).toBe(2);
|
expect(elapsed).toBe(2);
|
||||||
|
|||||||
@@ -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,13 +4,12 @@ import {
|
|||||||
OntimeBlock,
|
OntimeBlock,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
|
||||||
Playback,
|
Playback,
|
||||||
SupportedEvent,
|
SupportedEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { generateId } from 'ontime-utils';
|
import { generateId, getCueCandidate } from 'ontime-utils';
|
||||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { block as blockDef, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js';
|
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
||||||
import { MAX_EVENTS } from '../../settings.js';
|
import { MAX_EVENTS } from '../../settings.js';
|
||||||
import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
|
import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
|
||||||
import { eventTimer } from '../TimerService.js';
|
import { eventTimer } from '../TimerService.js';
|
||||||
@@ -18,13 +17,16 @@ 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,
|
||||||
cachedReorder,
|
cachedReorder,
|
||||||
|
cachedSwap,
|
||||||
delayedRundownCacheKey,
|
delayedRundownCacheKey,
|
||||||
} from './delayedRundown.utils.js';
|
} from './delayedRundown.utils.js';
|
||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
|
import { validateEvent } from '../../utils/parser.js';
|
||||||
import { clock } from '../Clock.js';
|
import { clock } from '../Clock.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,30 +165,30 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
|||||||
let newEvent: Partial<OntimeBaseEvent> = {};
|
let newEvent: Partial<OntimeBaseEvent> = {};
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
// TODO: filter the parameters that exist in the event, use the parserUtils
|
|
||||||
switch (eventData.type) {
|
|
||||||
case SupportedEvent.Event:
|
|
||||||
newEvent = { ...eventDef, ...eventData, id };
|
|
||||||
break;
|
|
||||||
case SupportedEvent.Delay:
|
|
||||||
newEvent = { ...delayDef, ...eventData, id };
|
|
||||||
break;
|
|
||||||
case SupportedEvent.Block:
|
|
||||||
newEvent = { ...blockDef, ...eventData, id };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let insertIndex = 0;
|
let insertIndex = 0;
|
||||||
if (typeof newEvent?.after !== 'undefined') {
|
if (eventData?.after !== undefined) {
|
||||||
const index = DataProvider.getIndexOf(newEvent.after);
|
const index = DataProvider.getIndexOf(eventData.after);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
logger.warning(LogOrigin.Server, `Could not find event with id ${newEvent.after}`);
|
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.after}`);
|
||||||
} else {
|
} else {
|
||||||
insertIndex = index + 1;
|
insertIndex = index + 1;
|
||||||
}
|
}
|
||||||
delete newEvent.after;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (eventData.type) {
|
||||||
|
case SupportedEvent.Event: {
|
||||||
|
newEvent = validateEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SupportedEvent.Delay:
|
||||||
|
newEvent = { ...delayDef, duration: eventData.duration, id } as OntimeDelay;
|
||||||
|
break;
|
||||||
|
case SupportedEvent.Block:
|
||||||
|
newEvent = { ...blockDef, title: eventData.title, id } as OntimeBlock;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
delete eventData.after;
|
||||||
|
|
||||||
// modify rundown
|
// modify rundown
|
||||||
await cachedAdd(insertIndex, newEvent as OntimeEvent | OntimeDelay | OntimeBlock);
|
await cachedAdd(insertIndex, newEvent as OntimeEvent | OntimeDelay | OntimeBlock);
|
||||||
|
|
||||||
@@ -203,6 +205,10 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
||||||
|
if (eventData.type === SupportedEvent.Event && eventData?.cue === '') {
|
||||||
|
throw new Error(`Cue value invalid`);
|
||||||
|
}
|
||||||
|
|
||||||
const newEvent = await cachedEdit(eventData.id, eventData);
|
const newEvent = await cachedEdit(eventData.id, eventData);
|
||||||
|
|
||||||
// notify timer service of changed events
|
// notify timer service of changed events
|
||||||
@@ -262,62 +268,30 @@ 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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* applies delay value for given event
|
* swaps two events
|
||||||
* @param eventId
|
* @param {string} from - id of event from
|
||||||
|
* @param {string} to - id of event to
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function applyDelay(eventId: string) {
|
export async function swapEvents(from: string, to: string) {
|
||||||
const rundown: OntimeRundown = DataProvider.getRundown();
|
await cachedSwap(from, to);
|
||||||
const { delayIndex, updatedRundown } = _applyDelay(eventId, rundown);
|
|
||||||
if (delayIndex === null) {
|
|
||||||
throw new Error(`Delay event with ID ${eventId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await DataProvider.setRundown(updatedRundown);
|
// notify timer service of changed events
|
||||||
await deleteEvent(eventId);
|
updateTimer();
|
||||||
|
|
||||||
|
// advice socket subscribers of change
|
||||||
|
sendRefetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
import { EndAction, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
import { EndAction, OntimeEvent, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { calculateRuntimeDelays, calculateRuntimeDelaysFrom, getDelayAt } from '../delayedRundown.utils.js';
|
import { calculateRuntimeDelays, calculateRuntimeDelaysFrom, getDelayAt } from '../delayedRundown.utils.js';
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '659e1',
|
id: '659e1',
|
||||||
|
cue: '1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 600000,
|
duration: 600000,
|
||||||
@@ -64,6 +65,7 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1c48f',
|
id: '1c48f',
|
||||||
|
cue: '2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 1200000,
|
duration: 1200000,
|
||||||
@@ -97,6 +99,7 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: 'd48c2',
|
id: 'd48c2',
|
||||||
|
cue: '3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -129,16 +132,17 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '2f185',
|
id: '2f185',
|
||||||
|
cue: '4',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = calculateRuntimeDelays(rundown);
|
const updatedRundown = calculateRuntimeDelays(rundown);
|
||||||
|
|
||||||
expect(rundown.length).toBe(updatedRundown.length);
|
expect(rundown.length).toBe(updatedRundown.length);
|
||||||
expect(updatedRundown[0].delay).toBe(0);
|
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
|
||||||
expect(updatedRundown[2].delay).toBe(600000);
|
expect((updatedRundown[2] as OntimeEvent).delay).toBe(600000);
|
||||||
expect(updatedRundown[4].delay).toBe(600000 + 1200000);
|
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
|
||||||
expect(updatedRundown[6].delay).toBe(0);
|
expect((updatedRundown[6] as OntimeEvent).delay).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,6 +175,7 @@ describe('getDelayAt()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: '659e1',
|
id: '659e1',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
cue: '1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 600000,
|
duration: 600000,
|
||||||
@@ -205,6 +210,7 @@ describe('getDelayAt()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1c48f',
|
id: '1c48f',
|
||||||
delay: 600000,
|
delay: 600000,
|
||||||
|
cue: '2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 1200000,
|
duration: 1200000,
|
||||||
@@ -239,6 +245,7 @@ describe('getDelayAt()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: 'd48c2',
|
id: 'd48c2',
|
||||||
delay: 1800000,
|
delay: 1800000,
|
||||||
|
cue: '3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -272,6 +279,7 @@ describe('getDelayAt()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: '2f185',
|
id: '2f185',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
cue: '4',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -331,6 +339,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: '659e1',
|
id: '659e1',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
cue: '1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 600000,
|
duration: 600000,
|
||||||
@@ -365,6 +374,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1c48f',
|
id: '1c48f',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
cue: '2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 1200000,
|
duration: 1200000,
|
||||||
@@ -399,6 +409,7 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: 'd48c2',
|
id: 'd48c2',
|
||||||
delay: 1800000,
|
delay: 1800000,
|
||||||
|
cue: '3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -432,14 +443,15 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
id: '2f185',
|
id: '2f185',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
cue: '4',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = calculateRuntimeDelaysFrom('07986', delayedRundown);
|
const updatedRundown = calculateRuntimeDelaysFrom('07986', delayedRundown);
|
||||||
|
|
||||||
// we only update from the 4th on
|
// we only update from the 4th on
|
||||||
expect(updatedRundown[0].delay).toBe(0);
|
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
|
||||||
// 1 + 3
|
// 1 + 3
|
||||||
expect(updatedRundown[4].delay).toBe(600000 + 1200000);
|
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +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 { _applyDelay } from '../delayUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key of rundown in cache
|
* Key of rundown in cache
|
||||||
@@ -49,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 {
|
||||||
@@ -77,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);
|
||||||
}
|
}
|
||||||
@@ -122,13 +132,13 @@ export async function cachedDelete(eventId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let updatedRundown = DataProvider.getRundown();
|
let updatedRundown = DataProvider.getRundown();
|
||||||
const eventType = updatedRundown[eventIndex].type;
|
const eventBack = { ...updatedRundown[eventIndex] };
|
||||||
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(eventBack) || isOntimeBlock(eventBack)) {
|
||||||
// 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);
|
||||||
@@ -174,7 +184,46 @@ export async function cachedReorder(eventId: string, from: number, to: number) {
|
|||||||
export async function cachedClear() {
|
export async function cachedClear() {
|
||||||
await DataProvider.clearRundown();
|
await DataProvider.clearRundown();
|
||||||
runtimeCacheStore.setCached(delayedRundownCacheKey, []);
|
runtimeCacheStore.setCached(delayedRundownCacheKey, []);
|
||||||
console.log(DataProvider.getRundown(), getDelayedRundown());
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps two events
|
||||||
|
* @param {string} fromEventId
|
||||||
|
* @param {string} toEventId
|
||||||
|
*/
|
||||||
|
export async function cachedSwap(fromEventId: string, toEventId: string) {
|
||||||
|
const fromEventIndex = DataProvider.getIndexOf(fromEventId);
|
||||||
|
const toEventIndex = DataProvider.getIndexOf(toEventId);
|
||||||
|
|
||||||
|
const rundown = DataProvider.getRundown();
|
||||||
|
const rundownToUpdate = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
|
||||||
|
|
||||||
|
const delayedRundown = getDelayedRundown();
|
||||||
|
const fromCachedEvent = delayedRundown.at(fromEventIndex);
|
||||||
|
const toCachedEvent = delayedRundown.at(toEventIndex);
|
||||||
|
|
||||||
|
if (fromCachedEvent.id !== fromEventId || toCachedEvent.id !== toEventId) {
|
||||||
|
// something went wrong, we invalidate the cache
|
||||||
|
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||||
|
} else {
|
||||||
|
const delayedRundownToUpdate = swapOntimeEvents(delayedRundown, fromEventIndex, toEventIndex);
|
||||||
|
runtimeCacheStore.setCached(delayedRundownCacheKey, delayedRundownToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,11 +235,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,
|
||||||
@@ -215,15 +264,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,
|
||||||
@@ -256,11 +305,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;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber, TimerType } from 'ontime-types';
|
||||||
import { dayInMs } from 'ontime-utils';
|
import { dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,6 +10,8 @@ export function getExpectedFinish(
|
|||||||
duration: number,
|
duration: number,
|
||||||
pausedTime: number,
|
pausedTime: number,
|
||||||
addedTime: number,
|
addedTime: number,
|
||||||
|
timeEnd: number,
|
||||||
|
timerType: TimerType,
|
||||||
) {
|
) {
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -19,6 +21,10 @@ export function getExpectedFinish(
|
|||||||
return finishedAt;
|
return finishedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timerType === TimerType.TimeToEnd) {
|
||||||
|
return timeEnd + addedTime + pausedTime;
|
||||||
|
}
|
||||||
|
|
||||||
// handle events that finish the day after
|
// handle events that finish the day after
|
||||||
const expectedFinish = startedAt + duration + pausedTime + addedTime;
|
const expectedFinish = startedAt + duration + pausedTime + addedTime;
|
||||||
if (expectedFinish > dayInMs) {
|
if (expectedFinish > dayInMs) {
|
||||||
@@ -38,11 +44,22 @@ export function getCurrent(
|
|||||||
addedTime: number,
|
addedTime: number,
|
||||||
pausedTime: number,
|
pausedTime: number,
|
||||||
clock: number,
|
clock: number,
|
||||||
|
timeEnd: number,
|
||||||
|
timerType: TimerType,
|
||||||
) {
|
) {
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timerType === TimerType.TimeToEnd) {
|
||||||
|
if (startedAt > timeEnd) {
|
||||||
|
return timeEnd + addedTime + pausedTime + dayInMs - clock;
|
||||||
|
}
|
||||||
|
return timeEnd + addedTime + pausedTime - clock;
|
||||||
|
}
|
||||||
|
|
||||||
if (startedAt > clock) {
|
if (startedAt > clock) {
|
||||||
|
// we are the day after the event was started
|
||||||
return startedAt + duration + addedTime + pausedTime - clock - dayInMs;
|
return startedAt + duration + addedTime + pausedTime - clock - dayInMs;
|
||||||
}
|
}
|
||||||
return startedAt + duration + addedTime + pausedTime - clock;
|
return startedAt + duration + addedTime + pausedTime - clock;
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import { getPreviousPlayable } from '../eventUtils.js';
|
|
||||||
|
|
||||||
describe('getPreviousPlayable()', () => {
|
|
||||||
describe('given a list of events', () => {
|
|
||||||
it('finds the previous playable event', () => {
|
|
||||||
const events = [
|
|
||||||
{ id: 100, type: 'delay' },
|
|
||||||
{ id: 101, type: 'event', skip: true },
|
|
||||||
{ id: 102, type: 'event', skip: true },
|
|
||||||
{ id: 103, type: 'event', skip: false },
|
|
||||||
{ id: 'not-this', type: 'block' },
|
|
||||||
{ id: 104, type: 'event' },
|
|
||||||
];
|
|
||||||
const { index, id } = getPreviousPlayable(events, events[4].id);
|
|
||||||
expect(index).toBe(3);
|
|
||||||
expect(id).toBe(103);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handles common errors', () => {
|
|
||||||
it('returns null if id not found in list', () => {
|
|
||||||
const events = [
|
|
||||||
{ id: 0, type: 'delay' },
|
|
||||||
{ id: 1, type: 'event', skip: true },
|
|
||||||
{ id: 2, type: 'event', skip: true },
|
|
||||||
{ id: 3, type: 'event', skip: false },
|
|
||||||
{ id: 4, type: 'event' },
|
|
||||||
];
|
|
||||||
const { index, id } = getPreviousPlayable(events, 'no-valid-id');
|
|
||||||
expect(index).toBe(null);
|
|
||||||
expect(id).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null if there are no previous events to play', () => {
|
|
||||||
const events = [
|
|
||||||
{ id: 0, type: 'delay' },
|
|
||||||
{ id: 1, type: 'event', skip: true },
|
|
||||||
{ id: 2, type: 'event', skip: true },
|
|
||||||
{ id: 3, type: 'event', skip: true },
|
|
||||||
{ id: 4, type: 'event' },
|
|
||||||
];
|
|
||||||
const { index, id } = getPreviousPlayable(events, events[4].id);
|
|
||||||
expect(index).toBe(null);
|
|
||||||
expect(id).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null if list is empty', () => {
|
|
||||||
const events = [];
|
|
||||||
const { index, id } = getPreviousPlayable(events, 'made-up');
|
|
||||||
expect(index).toBe(null);
|
|
||||||
expect(id).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+30
-80
@@ -1,10 +1,11 @@
|
|||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import { dbModel } from '../../models/dataModel.ts';
|
|
||||||
import { parseExcel, parseJson, validateEvent } from '../parser.ts';
|
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||||
import { makeString } from '../parserUtils.ts';
|
|
||||||
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.ts';
|
import { dbModel } from '../../models/dataModel.js';
|
||||||
import { EndAction, TimerType } from 'ontime-types';
|
import { parseExcel, parseJson, validateEvent } from '../parser.js';
|
||||||
import { dayInMs } from 'ontime-utils';
|
import { makeString } from '../parserUtils.js';
|
||||||
|
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
|
||||||
|
|
||||||
describe('test json parser with valid def', () => {
|
describe('test json parser with valid def', () => {
|
||||||
const testData = {
|
const testData = {
|
||||||
@@ -220,64 +221,20 @@ describe('test json parser with valid def', () => {
|
|||||||
const first = parseResponse?.rundown[0];
|
const first = parseResponse?.rundown[0];
|
||||||
const expected = {
|
const expected = {
|
||||||
title: 'Guest Welcoming',
|
title: 'Guest Welcoming',
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
timeStart: 31500000,
|
|
||||||
timeEnd: 32400000,
|
|
||||||
duration: 32400000 - 31500000,
|
|
||||||
isPublic: false,
|
|
||||||
endAction: 'play-next',
|
|
||||||
timerType: 'clock',
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
type: 'event',
|
||||||
revision: 0,
|
|
||||||
id: '4b31',
|
id: '4b31',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
};
|
};
|
||||||
expect(first).toStrictEqual(expected);
|
expect(first).toMatchObject(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('second event is as a match', () => {
|
it('second event is as a match', () => {
|
||||||
const second = parseResponse?.rundown[1];
|
const second = parseResponse?.rundown[1];
|
||||||
const expected = {
|
const expected = {
|
||||||
title: 'Good Morning',
|
title: 'Good Morning',
|
||||||
subtitle: 'Days schedule',
|
|
||||||
presenter: 'Carlos Valente',
|
|
||||||
note: '',
|
|
||||||
timeStart: 32400000,
|
|
||||||
timeEnd: 36000000,
|
|
||||||
endAction: 'play-next',
|
|
||||||
timerType: 'count-up',
|
|
||||||
duration: 36000000 - 32400000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: true,
|
|
||||||
colour: 'red',
|
|
||||||
type: 'event',
|
type: 'event',
|
||||||
revision: 0,
|
|
||||||
id: 'f24d',
|
id: 'f24d',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
};
|
};
|
||||||
expect(second).toStrictEqual(expected);
|
expect(second).toMatchObject(expected);
|
||||||
});
|
});
|
||||||
it('third event end action is set as the default value', () => {
|
it('third event end action is set as the default value', () => {
|
||||||
const third = parseResponse?.rundown[2];
|
const third = parseResponse?.rundown[2];
|
||||||
@@ -448,7 +405,7 @@ describe('test corrupt data', () => {
|
|||||||
it('handles missing event data', async () => {
|
it('handles missing event data', async () => {
|
||||||
const emptyEventData = {
|
const emptyEventData = {
|
||||||
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
|
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
|
||||||
event: {},
|
eventData: {},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: 2,
|
version: 2,
|
||||||
@@ -459,7 +416,7 @@ describe('test corrupt data', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const parsedDef = await parseJson(emptyEventData);
|
const parsedDef = await parseJson(emptyEventData);
|
||||||
expect(parsedDef.event).toStrictEqual(dbModel.event);
|
expect(parsedDef.eventData).toStrictEqual(dbModel.eventData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles missing settings', async () => {
|
it('handles missing settings', async () => {
|
||||||
@@ -488,7 +445,7 @@ describe('test event validator', () => {
|
|||||||
const event = {
|
const event = {
|
||||||
title: 'test',
|
title: 'test',
|
||||||
};
|
};
|
||||||
const validated = validateEvent(event);
|
const validated = validateEvent(event, 'test');
|
||||||
|
|
||||||
expect(validated).toEqual(
|
expect(validated).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -503,6 +460,7 @@ describe('test event validator', () => {
|
|||||||
revision: expect.any(Number),
|
revision: expect.any(Number),
|
||||||
type: expect.any(String),
|
type: expect.any(String),
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
cue: 'test',
|
||||||
colour: expect.any(String),
|
colour: expect.any(String),
|
||||||
user0: expect.any(String),
|
user0: expect.any(String),
|
||||||
user1: expect.any(String),
|
user1: expect.any(String),
|
||||||
@@ -520,7 +478,7 @@ describe('test event validator', () => {
|
|||||||
|
|
||||||
it('fails an empty object', () => {
|
it('fails an empty object', () => {
|
||||||
const event = {};
|
const event = {};
|
||||||
const validated = validateEvent(event);
|
const validated = validateEvent(event, 'none');
|
||||||
expect(validated).toEqual(null);
|
expect(validated).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -531,7 +489,8 @@ describe('test event validator', () => {
|
|||||||
presenter: 3.2,
|
presenter: 3.2,
|
||||||
note: '1899-12-30T08:00:10.000Z',
|
note: '1899-12-30T08:00:10.000Z',
|
||||||
};
|
};
|
||||||
const validated = validateEvent(event);
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
|
const validated = validateEvent(event, 'not-used');
|
||||||
expect(typeof validated.title).toEqual('string');
|
expect(typeof validated.title).toEqual('string');
|
||||||
expect(typeof validated.subtitle).toEqual('string');
|
expect(typeof validated.subtitle).toEqual('string');
|
||||||
expect(typeof validated.presenter).toEqual('string');
|
expect(typeof validated.presenter).toEqual('string');
|
||||||
@@ -543,6 +502,7 @@ describe('test event validator', () => {
|
|||||||
timeStart: false,
|
timeStart: false,
|
||||||
timeEnd: '2',
|
timeEnd: '2',
|
||||||
};
|
};
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
const validated = validateEvent(event);
|
const validated = validateEvent(event);
|
||||||
expect(typeof validated.timeStart).toEqual('number');
|
expect(typeof validated.timeStart).toEqual('number');
|
||||||
expect(validated.timeStart).toEqual(0);
|
expect(validated.timeStart).toEqual(0);
|
||||||
@@ -554,6 +514,7 @@ describe('test event validator', () => {
|
|||||||
const event = {
|
const event = {
|
||||||
title: {},
|
title: {},
|
||||||
};
|
};
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
const validated = validateEvent(event);
|
const validated = validateEvent(event);
|
||||||
expect(typeof validated.title).toEqual('string');
|
expect(typeof validated.title).toEqual('string');
|
||||||
});
|
});
|
||||||
@@ -571,11 +532,13 @@ describe('test makeString function', () => {
|
|||||||
converted = makeString(val);
|
converted = makeString(val);
|
||||||
expect(converted).toBe(expected);
|
expect(converted).toBe(expected);
|
||||||
|
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
val = ['testing'];
|
val = ['testing'];
|
||||||
expected = 'testing';
|
expected = 'testing';
|
||||||
converted = makeString(val);
|
converted = makeString(val);
|
||||||
expect(converted).toBe(expected);
|
expect(converted).toBe(expected);
|
||||||
|
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
val = { doing: 'testing' };
|
val = { doing: 'testing' };
|
||||||
converted = makeString(val, 'fallback');
|
converted = makeString(val, 'fallback');
|
||||||
expect(converted).toBe('fallback');
|
expect(converted).toBe('fallback');
|
||||||
@@ -628,10 +591,6 @@ describe('test parseExcel function', () => {
|
|||||||
'x',
|
'x',
|
||||||
'',
|
'',
|
||||||
'Ballyhoo',
|
'Ballyhoo',
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'a0',
|
'a0',
|
||||||
'a1',
|
'a1',
|
||||||
'a2',
|
'a2',
|
||||||
@@ -650,15 +609,11 @@ describe('test parseExcel function', () => {
|
|||||||
'A song from the hearth',
|
'A song from the hearth',
|
||||||
'Still Carlos',
|
'Still Carlos',
|
||||||
'Derailing early',
|
'Derailing early',
|
||||||
'clock',
|
|
||||||
'load-next',
|
'load-next',
|
||||||
|
'clock',
|
||||||
'',
|
'',
|
||||||
'',
|
'x',
|
||||||
'Rainbow chase',
|
'Rainbow chase',
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'b0',
|
'b0',
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
@@ -682,10 +637,11 @@ describe('test parseExcel function', () => {
|
|||||||
backstageInfo: 'test backstage info',
|
backstageInfo: 'test backstage info',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: update tests once import is resolved
|
||||||
const expectedParsedRundown = [
|
const expectedParsedRundown = [
|
||||||
{
|
{
|
||||||
timeStart: 25200000,
|
//timeStart: 28800000,
|
||||||
timeEnd: 28810000,
|
//timeEnd: 32410000,
|
||||||
title: 'Guest Welcome',
|
title: 'Guest Welcome',
|
||||||
presenter: 'Carlos',
|
presenter: 'Carlos',
|
||||||
subtitle: 'Getting things started',
|
subtitle: 'Getting things started',
|
||||||
@@ -708,8 +664,8 @@ describe('test parseExcel function', () => {
|
|||||||
type: 'event',
|
type: 'event',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeStart: 28800000,
|
//timeStart: 32400000,
|
||||||
timeEnd: 30600000,
|
//timeEnd: 34200000,
|
||||||
title: 'A song from the hearth',
|
title: 'A song from the hearth',
|
||||||
presenter: 'Still Carlos',
|
presenter: 'Still Carlos',
|
||||||
subtitle: 'Derailing early',
|
subtitle: 'Derailing early',
|
||||||
@@ -728,13 +684,8 @@ describe('test parseExcel function', () => {
|
|||||||
const parsedData = await parseExcel(testdata);
|
const parsedData = await parseExcel(testdata);
|
||||||
expect(parsedData.eventData).toStrictEqual(expectedParsedEvent);
|
expect(parsedData.eventData).toStrictEqual(expectedParsedEvent);
|
||||||
expect(parsedData.rundown).toBeDefined();
|
expect(parsedData.rundown).toBeDefined();
|
||||||
expect(parsedData.rundown.title).toBe(expectedParsedRundown.title);
|
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
||||||
expect(parsedData.rundown.presenter).toBe(expectedParsedRundown.presenter);
|
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
||||||
expect(parsedData.rundown.subtitle).toBe(expectedParsedRundown.subtitle);
|
|
||||||
expect(parsedData.rundown.isPublic).toBe(expectedParsedRundown.isPublic);
|
|
||||||
expect(parsedData.rundown.skip).toBe(expectedParsedRundown.skip);
|
|
||||||
expect(parsedData.rundown.note).toBe(expectedParsedRundown.note);
|
|
||||||
expect(parsedData.rundown.type).toBe(expectedParsedRundown.type);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -759,7 +710,6 @@ describe('test aliases import', () => {
|
|||||||
expect(parsed.length).toBe(1);
|
expect(parsed.length).toBe(1);
|
||||||
|
|
||||||
// generates missing id
|
// generates missing id
|
||||||
console.log(parsed);
|
|
||||||
expect(parsed[0].alias).toBeDefined();
|
expect(parsed[0].alias).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -874,7 +824,7 @@ describe('test views import', () => {
|
|||||||
endMessage: '',
|
endMessage: '',
|
||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
};
|
};
|
||||||
const parsed = parseViewSettings(testData);
|
const parsed = parseViewSettings(testData, false);
|
||||||
expect(parsed).toStrictEqual(expectedParsedViewSettings);
|
expect(parsed).toStrictEqual(expectedParsedViewSettings);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { parseExcelDate } from '../time';
|
|
||||||
|
|
||||||
describe('parseExcelDate', () => {
|
|
||||||
it('parses a valid date string as expected from excel', () => {
|
|
||||||
const millis = parseExcelDate('1899-12-30T07:00:00.000Z');
|
|
||||||
expect(millis).not.toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parses a time string that passes validation', () => {
|
|
||||||
const validFields = ['10:00:00', '10:00'];
|
|
||||||
validFields.forEach((field) => {
|
|
||||||
it(`handles ${field}`, () => {
|
|
||||||
const millis = parseExcelDate(field);
|
|
||||||
expect(millis).not.toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('returns 0 on other strings', () => {
|
|
||||||
const invalidFields = ['10', 'test', ''];
|
|
||||||
invalidFields.forEach((field) => {
|
|
||||||
it(`handles ${field}`, () => {
|
|
||||||
const millis = parseExcelDate(field);
|
|
||||||
expect(millis).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { parseExcelDate } from '../time.js';
|
||||||
|
|
||||||
|
describe('parseExcelDate', () => {
|
||||||
|
describe.todo('parses a valid date string as expected from excel', () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
fromExcel: '1899-12-30T00:00:00.000Z',
|
||||||
|
expected: 3600000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fromExcel: '1899-12-30T00:10:00.000Z',
|
||||||
|
expected: 4200000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fromExcel: '1899-12-30T01:00:00.000Z',
|
||||||
|
expected: 7200000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fromExcel: '1899-12-30T07:00:00.000Z',
|
||||||
|
expected: 28800000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fromExcel: '1899-12-30T08:00:10.000Z',
|
||||||
|
expected: 32410000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fromExcel: '1899-12-30T08:30:00.000Z',
|
||||||
|
expected: 34200000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const scenario of testCases) {
|
||||||
|
it(`handles ${scenario.fromExcel}`, () => {
|
||||||
|
expect(parseExcelDate(scenario.fromExcel)).toBe(scenario.expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parses a time string that passes validation', () => {
|
||||||
|
const validFields = ['10:00:00', '10:00'];
|
||||||
|
validFields.forEach((field) => {
|
||||||
|
it(`handles ${field}`, () => {
|
||||||
|
const millis = parseExcelDate(field);
|
||||||
|
expect(millis).not.toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('returns 0 on other strings', () => {
|
||||||
|
const invalidFields = ['10', 'test', ''];
|
||||||
|
invalidFields.forEach((field) => {
|
||||||
|
it(`handles ${field}`, () => {
|
||||||
|
const millis = parseExcelDate(field);
|
||||||
|
expect(millis).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { cleanURL } from '../url';
|
import { cleanURL } from '../url.js';
|
||||||
|
|
||||||
describe('url is correctly formatted', () => {
|
describe('url is correctly formatted', () => {
|
||||||
it('has no leading spaces', () => {
|
it('has no leading spaces', () => {
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* @description Returns id of previous played event
|
|
||||||
* @param {array} events
|
|
||||||
* @param {string} eventId
|
|
||||||
* @return {object}
|
|
||||||
*/
|
|
||||||
export function getPreviousPlayable(events, eventId) {
|
|
||||||
// find current index
|
|
||||||
const current = events.findIndex((event) => event.id === eventId);
|
|
||||||
|
|
||||||
if (current === -1) {
|
|
||||||
return { index: null, id: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = current - 1;
|
|
||||||
while (index >= 0) {
|
|
||||||
const event = events[index];
|
|
||||||
if (event.type === 'event' && !event.skip) {
|
|
||||||
return { index, id: event.id };
|
|
||||||
}
|
|
||||||
index--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { index: null, id: null };
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,16 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import xlsx from 'node-xlsx';
|
import xlsx from 'node-xlsx';
|
||||||
import { generateId, calculateDuration } from 'ontime-utils';
|
import { generateId, calculateDuration } from 'ontime-utils';
|
||||||
import { DatabaseModel, EventData, OntimeEvent, OntimeRundown, UserFields } from 'ontime-types';
|
import {
|
||||||
|
DatabaseModel,
|
||||||
|
EndAction,
|
||||||
|
EventData,
|
||||||
|
OntimeEvent,
|
||||||
|
OntimeRundown,
|
||||||
|
SupportedEvent,
|
||||||
|
TimerType,
|
||||||
|
UserFields,
|
||||||
|
} from 'ontime-types';
|
||||||
import { event as eventDef } from '../models/eventsDefinition.js';
|
import { event as eventDef } from '../models/eventsDefinition.js';
|
||||||
import { dbModel } from '../models/dataModel.js';
|
import { dbModel } from '../models/dataModel.js';
|
||||||
import { deleteFile, makeString } from './parserUtils.js';
|
import { deleteFile, makeString } from './parserUtils.js';
|
||||||
@@ -38,6 +47,7 @@ export const parseExcel = async (excelData) => {
|
|||||||
let timeStartIndex: number | null = null;
|
let timeStartIndex: number | null = null;
|
||||||
let timeEndIndex: number | null = null;
|
let timeEndIndex: number | null = null;
|
||||||
let titleIndex: number | null = null;
|
let titleIndex: number | null = null;
|
||||||
|
let cueIndex: number | null = null;
|
||||||
let presenterIndex: number | null = null;
|
let presenterIndex: number | null = null;
|
||||||
let subtitleIndex: number | null = null;
|
let subtitleIndex: number | null = null;
|
||||||
let isPublicIndex: number | null = null;
|
let isPublicIndex: number | null = null;
|
||||||
@@ -91,6 +101,8 @@ export const parseExcel = async (excelData) => {
|
|||||||
event.timeEnd = parseExcelDate(column);
|
event.timeEnd = parseExcelDate(column);
|
||||||
} else if (j === titleIndex) {
|
} else if (j === titleIndex) {
|
||||||
event.title = column;
|
event.title = column;
|
||||||
|
} else if (j === cueIndex) {
|
||||||
|
event.cue = column;
|
||||||
} else if (j === presenterIndex) {
|
} else if (j === presenterIndex) {
|
||||||
event.presenter = column;
|
event.presenter = column;
|
||||||
} else if (j === subtitleIndex) {
|
} else if (j === subtitleIndex) {
|
||||||
@@ -102,9 +114,17 @@ export const parseExcel = async (excelData) => {
|
|||||||
} else if (j === notesIndex) {
|
} else if (j === notesIndex) {
|
||||||
event.note = column;
|
event.note = column;
|
||||||
} else if (j === endActionIndex) {
|
} else if (j === endActionIndex) {
|
||||||
event.endAction = column;
|
if (column === '') {
|
||||||
|
event.endAction = EndAction.None;
|
||||||
|
} else {
|
||||||
|
event.endAction = column;
|
||||||
|
}
|
||||||
} else if (j === timerTypeIndex) {
|
} else if (j === timerTypeIndex) {
|
||||||
event.timerType = column;
|
if (column === '') {
|
||||||
|
event.timerType = TimerType.CountDown;
|
||||||
|
} else {
|
||||||
|
event.timerType = column;
|
||||||
|
}
|
||||||
} else if (j === colourIndex) {
|
} else if (j === colourIndex) {
|
||||||
event.colour = column;
|
event.colour = column;
|
||||||
} else if (j === user0Index) {
|
} else if (j === user0Index) {
|
||||||
@@ -130,6 +150,7 @@ export const parseExcel = async (excelData) => {
|
|||||||
} else {
|
} else {
|
||||||
if (typeof column === 'string') {
|
if (typeof column === 'string') {
|
||||||
const col = column.toLowerCase();
|
const col = column.toLowerCase();
|
||||||
|
|
||||||
// look for keywords
|
// look for keywords
|
||||||
// need to make sure it is a string first
|
// need to make sure it is a string first
|
||||||
switch (col) {
|
switch (col) {
|
||||||
@@ -157,6 +178,10 @@ export const parseExcel = async (excelData) => {
|
|||||||
case 'finish':
|
case 'finish':
|
||||||
timeEndIndex = j;
|
timeEndIndex = j;
|
||||||
break;
|
break;
|
||||||
|
case 'cue':
|
||||||
|
case 'page':
|
||||||
|
cueIndex = j;
|
||||||
|
break;
|
||||||
case 'event title':
|
case 'event title':
|
||||||
case 'title':
|
case 'title':
|
||||||
titleIndex = j;
|
titleIndex = j;
|
||||||
@@ -243,7 +268,7 @@ export const parseExcel = async (excelData) => {
|
|||||||
if (Object.keys(event).length > 0) {
|
if (Object.keys(event).length > 0) {
|
||||||
// if any data was found, push to array
|
// if any data was found, push to array
|
||||||
// take care of it in the next step
|
// take care of it in the next step
|
||||||
rundown.push({ ...event, type: 'event' });
|
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -284,7 +309,6 @@ export const parseJson = async (jsonData, enforce = false): Promise<DatabaseMode
|
|||||||
// Import user fields if any
|
// Import user fields if any
|
||||||
returnData.userFields = parseUserFields(jsonData);
|
returnData.userFields = parseUserFields(jsonData);
|
||||||
// Import OSC settings if any
|
// Import OSC settings if any
|
||||||
// @ts-expect-error -- we are unable to type just yet
|
|
||||||
returnData.osc = parseOsc(jsonData, enforce);
|
returnData.osc = parseOsc(jsonData, enforce);
|
||||||
// Import HTTP settings if any
|
// Import HTTP settings if any
|
||||||
// returnData.http = parseHttp(jsonData, enforce);
|
// returnData.http = parseHttp(jsonData, enforce);
|
||||||
@@ -295,12 +319,15 @@ export const parseJson = async (jsonData, enforce = false): Promise<DatabaseMode
|
|||||||
/**
|
/**
|
||||||
* @description Enforces formatting for events
|
* @description Enforces formatting for events
|
||||||
* @param {object} eventArgs - attributes of event
|
* @param {object} eventArgs - attributes of event
|
||||||
|
* @param cueFallback
|
||||||
* @returns {object|null} - formatted object or null in case is invalid
|
* @returns {object|null} - formatted object or null in case is invalid
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const validateEvent = (eventArgs) => {
|
export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string) => {
|
||||||
// ensure id is defined and unique
|
// ensure id is defined and unique
|
||||||
const id = eventArgs.id || generateId();
|
const id = eventArgs.id || generateId();
|
||||||
|
const cue = eventArgs.cue || cueFallback;
|
||||||
|
|
||||||
let event = null;
|
let event = null;
|
||||||
|
|
||||||
// return if object is empty
|
// return if object is empty
|
||||||
@@ -336,12 +363,9 @@ export const validateEvent = (eventArgs) => {
|
|||||||
user7: makeString(e.user7, d.user7),
|
user7: makeString(e.user7, d.user7),
|
||||||
user8: makeString(e.user8, d.user8),
|
user8: makeString(e.user8, d.user8),
|
||||||
user9: makeString(e.user9, d.user9),
|
user9: makeString(e.user9, d.user9),
|
||||||
// deciding not to validate colour
|
|
||||||
// this adds flexibility to the user to write hex codes, rgb,
|
|
||||||
// but also colour names like blue and red
|
|
||||||
// CSS.supports is only available in frontend
|
|
||||||
colour: makeString(e.colour, d.colour),
|
colour: makeString(e.colour, d.colour),
|
||||||
id,
|
id,
|
||||||
|
cue,
|
||||||
type: 'event',
|
type: 'event',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -357,7 +381,7 @@ type ResponseError = { error: true; message: string };
|
|||||||
* @param {string} file - reference to file
|
* @param {string} file - reference to file
|
||||||
* @return {object} - parse result message
|
* @return {object} - parse result message
|
||||||
*/
|
*/
|
||||||
export const fileHandler = async (file): ResponseOK | ResponseError => {
|
export const fileHandler = async (file): Promise<ResponseOK | ResponseError> => {
|
||||||
let res: Partial<ResponseOK | ResponseError> = {};
|
let res: Partial<ResponseOK | ResponseError> = {};
|
||||||
|
|
||||||
// check which file type are we dealing with
|
// check which file type are we dealing with
|
||||||
@@ -376,8 +400,7 @@ export const fileHandler = async (file): ResponseOK | ResponseError => {
|
|||||||
res.data.userFields = parseUserFields(dataFromExcel);
|
res.data.userFields = parseUserFields(dataFromExcel);
|
||||||
res.message = 'success';
|
res.message = 'success';
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = 'No sheet found named ontime or event schedule';
|
const errorMessage = 'No sheet found named "ontime" or "event schedule"';
|
||||||
console.log(errorMessage);
|
|
||||||
res = {
|
res = {
|
||||||
error: true,
|
error: true,
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const parseRundown = (data): OntimeRundown => {
|
|||||||
console.log('Found rundown definition, importing...');
|
console.log('Found rundown definition, importing...');
|
||||||
const rundown = [];
|
const rundown = [];
|
||||||
try {
|
try {
|
||||||
|
let eventIndex = 0;
|
||||||
const ids = [];
|
const ids = [];
|
||||||
for (const e of data.rundown) {
|
for (const e of data.rundown) {
|
||||||
// cap number of events
|
// cap number of events
|
||||||
@@ -43,6 +44,7 @@ export const parseRundown = (data): OntimeRundown => {
|
|||||||
console.log('ERROR: ID collision on import, skipping');
|
console.log('ERROR: ID collision on import, skipping');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the right endAction is used
|
// validate the right endAction is used
|
||||||
if (e.endAction && !Object.values(EndAction).includes(e.endAction)) {
|
if (e.endAction && !Object.values(EndAction).includes(e.endAction)) {
|
||||||
e.endAction = EndAction.None;
|
e.endAction = EndAction.None;
|
||||||
@@ -54,8 +56,10 @@ export const parseRundown = (data): OntimeRundown => {
|
|||||||
e.timerType = TimerType.CountDown;
|
e.timerType = TimerType.CountDown;
|
||||||
console.log('WARNING: invalid Timer Type provided, using default');
|
console.log('WARNING: invalid Timer Type provided, using default');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.type === 'event') {
|
if (e.type === 'event') {
|
||||||
const event = validateEvent(e);
|
eventIndex += 1;
|
||||||
|
const event = validateEvent(e, eventIndex.toString());
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
rundown.push(event);
|
rundown.push(event);
|
||||||
ids.push(event.id);
|
ids.push(event.id);
|
||||||
@@ -126,6 +130,7 @@ export const parseSettings = (data, enforce): Settings => {
|
|||||||
console.log('ERROR: unknown app version, skipping');
|
console.log('ERROR: unknown app version, skipping');
|
||||||
} else {
|
} else {
|
||||||
const settings = {
|
const settings = {
|
||||||
|
serverPort: s.serverPort || dbModel.settings.serverPort,
|
||||||
editorKey: s.editorKey || null,
|
editorKey: s.editorKey || null,
|
||||||
operatorKey: s.operatorKey || null,
|
operatorKey: s.operatorKey || null,
|
||||||
timeFormat: s.timeFormat || '24',
|
timeFormat: s.timeFormat || '24',
|
||||||
@@ -216,7 +221,12 @@ export const validateOscObject = (data: OscSubscription): boolean => {
|
|||||||
/**
|
/**
|
||||||
* Parse osc portion of an entry
|
* Parse osc portion of an entry
|
||||||
*/
|
*/
|
||||||
export const parseOsc = (data: { osc?: Partial<OSCSettings> }, enforce: boolean): Partial<OSCSettings> => {
|
export const parseOsc = (
|
||||||
|
data: {
|
||||||
|
osc?: Partial<OSCSettings>;
|
||||||
|
},
|
||||||
|
enforce: boolean,
|
||||||
|
): OSCSettings | Record<string, never> => {
|
||||||
if ('osc' in data) {
|
if ('osc' in data) {
|
||||||
console.log('Found OSC definition, importing...');
|
console.log('Found OSC definition, importing...');
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { dayInMs } from 'ontime-utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Ensures variable is string, it skips object types
|
* @description Ensures variable is string, it skips object types
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ export const timeFormat = 'HH:mm';
|
|||||||
export const timeFormatSeconds = 'HH:mm:ss';
|
export const timeFormatSeconds = 'HH:mm:ss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Converts an excel date to milliseconds
|
* @description Converts a date object to milliseconds
|
||||||
* @argument {string} date - excel string date
|
* @argument {Date} date
|
||||||
* @returns {number} - time in milliseconds
|
* @returns {number} - time in milliseconds
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const dateToMillis = (date: Date): number => {
|
export const dateToMillis = (date: Date): number => {
|
||||||
|
// TODO: Use UTC
|
||||||
const h = date.getHours();
|
const h = date.getHours();
|
||||||
const m = date.getMinutes();
|
const m = date.getMinutes();
|
||||||
const s = date.getSeconds();
|
const s = date.getSeconds();
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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');
|
||||||
|
await expect(page.getByText('+10 minNew start: 00:18:00')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('CRUD operations on the rundown', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:4001/editor');
|
||||||
|
|
||||||
|
// clear rundown
|
||||||
|
await page.getByRole('button', { name: 'Run mode' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Event...' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete all events' }).click();
|
||||||
|
|
||||||
|
// create event from the rundown empty button
|
||||||
|
await page.getByRole('button', { name: 'Create Event' }).click();
|
||||||
|
|
||||||
|
// create blocks using the quick add buttons
|
||||||
|
await page.getByRole('button', { name: 'Block' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Delay' }).click();
|
||||||
|
await page.getByTestId('quick-add-event').click();
|
||||||
|
|
||||||
|
// test quick add options - start is last end
|
||||||
|
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').fill('20m');
|
||||||
|
await page.getByText('Start time is last end').click();
|
||||||
|
await page.getByTestId('quick-add-event').click();
|
||||||
|
await expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain(
|
||||||
|
'00:20:00',
|
||||||
|
);
|
||||||
|
|
||||||
|
// test quick add options - event is public
|
||||||
|
await page.locator('label').filter({ hasText: 'Event is public' }).click();
|
||||||
|
await page.getByTestId('quick-add-event').click();
|
||||||
|
|
||||||
|
await expect(await page.getByTestId('entry-4').getByRole('img').nth(3)).toHaveAttribute('data-isPublic', 'true');
|
||||||
|
});
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.3.9",
|
"version": "2.7.2",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lighdev",
|
"lighdev",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum TimerType {
|
export enum TimerType {
|
||||||
CountDown = 'count-down',
|
CountDown = 'count-down',
|
||||||
CountUp = 'count-up',
|
CountUp = 'count-up',
|
||||||
Clock = 'clock'
|
TimeToEnd = 'time-to-end',
|
||||||
|
Clock = 'clock',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type OntimeBlock = OntimeBaseEvent & {
|
|||||||
|
|
||||||
export type OntimeEvent = OntimeBaseEvent & {
|
export type OntimeEvent = OntimeBaseEvent & {
|
||||||
type: SupportedEvent.Event;
|
type: SupportedEvent.Event;
|
||||||
|
cue: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
presenter: string;
|
presenter: string;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user