mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-08 08:53:51 +00:00
refactor: remove unused and legacy code
- remove legacy migrations - remove unused server code - remove unused UI code
This commit is contained in:
@@ -10,7 +10,6 @@ export const PROJECT_DATA = ['project'];
|
||||
export const PROJECT_LIST = ['projectList'];
|
||||
export const RUNDOWN = ['rundown'];
|
||||
export const RUNTIME = ['runtimeStore'];
|
||||
export const SHEET_STATE = ['sheetState'];
|
||||
export const URL_PRESETS = ['urlpresets'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
export const CLIENT_LIST = ['clientList'];
|
||||
@@ -19,11 +18,8 @@ export const REPORT = ['report'];
|
||||
// API URLs
|
||||
export const apiEntryUrl = `${serverURL}/data`;
|
||||
|
||||
export const projectDataURL = `${serverURL}/project`;
|
||||
export const rundownURL = `${serverURL}/events`;
|
||||
export const ontimeURL = `${serverURL}/ontime`;
|
||||
const userAssetsPath = 'user';
|
||||
const cssOverridePath = 'styles/override.css';
|
||||
|
||||
export const userAssetsPath = 'user';
|
||||
export const cssOverridePath = 'styles/override.css';
|
||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||
|
||||
@@ -23,7 +23,7 @@ export type OptionWithoutGroup = {
|
||||
withDivider?: boolean;
|
||||
};
|
||||
|
||||
export type OptionWithGroup = {
|
||||
type OptionWithGroup = {
|
||||
label: string;
|
||||
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@ import React from 'react';
|
||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||
import * as Sentry from '@sentry/react';
|
||||
|
||||
import { runtimeStore } from '@/common/stores/runtime';
|
||||
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
||||
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
|
||||
import { runtimeStore } from '../../stores/runtime';
|
||||
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
|
||||
@@ -37,7 +37,7 @@ class ErrorBoundary extends React.Component {
|
||||
scope.setExtras({
|
||||
error,
|
||||
store: appState,
|
||||
hasSocket: { hasConnected, shouldReconnect, reconnectAttempts },
|
||||
hasSocket: { hasConnected, reconnectAttempts },
|
||||
});
|
||||
const eventId = Sentry.captureException(error);
|
||||
this.setState({ eventId, info });
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
$loader-size: 4rem;
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: $black-10;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
|
||||
.loader {
|
||||
width: $loader-size;
|
||||
height: $loader-size;
|
||||
background: $blue-500;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
animation: animloader 1s ease-in infinite;
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import style from './LoaderOverlay.module.scss';
|
||||
|
||||
export default function LoaderOverlay() {
|
||||
return (
|
||||
<div className={style.overlay}>
|
||||
<span className={style.loader} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import NavigationMenu from './NavigationMenu';
|
||||
|
||||
interface ProductionNavigationMenuProps {
|
||||
isMenuOpen: boolean;
|
||||
onMenuClose: () => void;
|
||||
}
|
||||
|
||||
function ProductionNavigationMenu(props: ProductionNavigationMenuProps) {
|
||||
const { isMenuOpen, onMenuClose } = props;
|
||||
|
||||
return <NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />;
|
||||
}
|
||||
|
||||
export default memo(ProductionNavigationMenu);
|
||||
@@ -12,7 +12,7 @@ type OptionsField = {
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export type MultiselectOption = { value: string; label: string; colour: string };
|
||||
type MultiselectOption = { value: string; label: string; colour: string };
|
||||
export type MultiselectOptions = Record<string, MultiselectOption>;
|
||||
type MultiOptionsField = {
|
||||
type: 'multi-option';
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// roughly from https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
function getMatches(query: string): boolean {
|
||||
return window.matchMedia(query).matches;
|
||||
}
|
||||
|
||||
// TODO: debounce handleChange
|
||||
export default function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState<boolean>(getMatches(query));
|
||||
|
||||
const handleChange = useCallback(() => {
|
||||
setMatches(getMatches(query));
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
const matchMedia = window.matchMedia(query);
|
||||
|
||||
// Triggered at the first client-side load and if query changes
|
||||
handleChange();
|
||||
|
||||
// Listen matchMedia
|
||||
matchMedia.addEventListener('change', handleChange);
|
||||
|
||||
return () => {
|
||||
matchMedia.removeEventListener('change', handleChange);
|
||||
};
|
||||
}, [handleChange, query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { editAutomationSettings, getAutomationSettings } from '../api/automation';
|
||||
import { getAutomationSettings } from '../api/automation';
|
||||
import { AUTOMATION } from '../api/constants';
|
||||
import { logAxiosError } from '../api/utils';
|
||||
import { automationPlaceholderSettings } from '../models/AutomationSettings';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
export default function useAutomationSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
@@ -20,15 +18,3 @@ export default function useAutomationSettings() {
|
||||
|
||||
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
export function useAutomationSettingsMutation() {
|
||||
const { isPending, mutateAsync } = useMutation({
|
||||
mutationFn: editAutomationSettings,
|
||||
onError: (error) => logAxiosError('Error saving Automation settings', error),
|
||||
onSuccess: (data) => {
|
||||
ontimeQueryClient.setQueryData(AUTOMATION, data);
|
||||
},
|
||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: AUTOMATION }),
|
||||
});
|
||||
return { isPending, mutateAsync };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const placeholderProjectList: ProjectFileListResponse = {
|
||||
lastLoadedProject: '',
|
||||
};
|
||||
|
||||
export function useProjectList() {
|
||||
function useProjectList() {
|
||||
const { data, status, refetch } = useQuery({
|
||||
queryKey: PROJECT_LIST,
|
||||
queryFn: getProjects,
|
||||
|
||||
@@ -12,7 +12,7 @@ type noop = (this: any, ...args: any[]) => any;
|
||||
|
||||
type PickFunction<T extends noop> = (this: ThisParameterType<T>, ...args: Parameters<T>) => ReturnType<T>;
|
||||
|
||||
export const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
|
||||
const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
|
||||
|
||||
export default function useMemoisedFn<T extends noop>(fn: T) {
|
||||
if (isDev) {
|
||||
|
||||
@@ -91,14 +91,6 @@ export const setPlayback = {
|
||||
},
|
||||
};
|
||||
|
||||
export const useInfoPanel = createSelector((state: RuntimeStore) => ({
|
||||
eventNow: state.eventNow,
|
||||
eventNext: state.eventNext,
|
||||
playback: state.timer.playback,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
numEvents: state.runtime.numEvents,
|
||||
}));
|
||||
|
||||
export const useAuxTimerTime = createSelector((state: RuntimeStore) => state.auxtimer1.current);
|
||||
|
||||
export const useAuxTimerControl = createSelector((state: RuntimeStore) => ({
|
||||
@@ -145,8 +137,6 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({
|
||||
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||
}));
|
||||
|
||||
export const setClientName = (newName: string) => socketSendJson('set-client-name', newName);
|
||||
|
||||
export const useRuntimeOverview = createSelector((state: RuntimeStore) => ({
|
||||
plannedStart: state.runtime.plannedStart,
|
||||
actualStart: state.runtime.actualStart,
|
||||
|
||||
@@ -11,7 +11,7 @@ type LogStore = {
|
||||
logs: Log[];
|
||||
};
|
||||
|
||||
export const logger = createStore<LogStore>(() => ({
|
||||
const logger = createStore<LogStore>(() => ({
|
||||
logs: [],
|
||||
}));
|
||||
|
||||
|
||||
@@ -16,11 +16,10 @@ import { addDialog } from '../stores/dialogStore';
|
||||
import { addLog } from '../stores/logger';
|
||||
import { addToBatchUpdates, flushBatchUpdates, patchRuntime, patchRuntimeProperty } from '../stores/runtime';
|
||||
|
||||
export let websocket: WebSocket | null = null;
|
||||
let websocket: WebSocket | null = null;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
const reconnectInterval = 1000;
|
||||
|
||||
export let shouldReconnect = true;
|
||||
export let hasConnected = false;
|
||||
export let reconnectAttempts = 0;
|
||||
|
||||
@@ -50,15 +49,14 @@ export const connectSocket = () => {
|
||||
console.warn('WebSocket disconnected');
|
||||
setOnlineStatus(false);
|
||||
|
||||
if (shouldReconnect) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.warn('WebSocket: attempting reconnect');
|
||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||
reconnectAttempts += 1;
|
||||
connectSocket();
|
||||
}
|
||||
}, reconnectInterval);
|
||||
}
|
||||
// we decide to allows reconnect
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.warn('WebSocket: attempting reconnect');
|
||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||
reconnectAttempts += 1;
|
||||
connectSocket();
|
||||
}
|
||||
}, reconnectInterval);
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
@@ -224,11 +222,6 @@ export const connectSocket = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const disconnectSocket = () => {
|
||||
shouldReconnect = false;
|
||||
websocket?.close();
|
||||
};
|
||||
|
||||
export const socketSend = (message: any) => {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(message);
|
||||
|
||||
@@ -35,7 +35,7 @@ function getFormatFromParams() {
|
||||
* Gets the format options from the applicaton settings
|
||||
* @returns a string equivalent to the format, ie: hh:mm:ss a or HH:mm:ss
|
||||
*/
|
||||
export function getFormatFromSettings(): TimeFormat {
|
||||
function getFormatFromSettings(): TimeFormat {
|
||||
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||
return settings?.timeFormat ?? '24';
|
||||
}
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
|
||||
|
||||
import 'vitest';
|
||||
|
||||
// ugly hack because vite and pnpm are not playing ball with jest
|
||||
// https://github.com/testing-library/jest-dom/issues/123
|
||||
declare global {
|
||||
namespace Vi {
|
||||
type Assertion<T = any> = TestingLibraryMatchers<T, void>;
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,6 @@ import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import GeneralPinInput from './GeneralPinInput';
|
||||
|
||||
export type GeneralPanelFormValues = {
|
||||
filename: string;
|
||||
};
|
||||
|
||||
export default function GeneralPanelForm() {
|
||||
const { data, status, refetch } = useSettings();
|
||||
const {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { ImportCustom, ImportMap } from 'ontime-utils';
|
||||
export type NamedImportMap = typeof namedImportMap;
|
||||
|
||||
// Record of label and import name
|
||||
export const namedImportMap = {
|
||||
const namedImportMap = {
|
||||
Worksheet: 'event schedule',
|
||||
Start: 'time start',
|
||||
'Link start': 'link start',
|
||||
|
||||
@@ -16,8 +16,6 @@ import EventEditorEmpty from './EventEditorEmpty';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
|
||||
export type EventEditorSubmitActions = keyof OntimeEvent;
|
||||
|
||||
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
|
||||
|
||||
interface EventEditorProps {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { RUNDOWN } from '../../common/api/constants';
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
import { isMacOS } from '../../common/utils/deviceUtils';
|
||||
|
||||
export type SelectionMode = 'shift' | 'click' | 'ctrl';
|
||||
type SelectionMode = 'shift' | 'click' | 'ctrl';
|
||||
|
||||
interface EventSelectionStore {
|
||||
selectedEvents: Set<string>;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// used in both sm and public views
|
||||
export const titleVariants = {
|
||||
hidden: {
|
||||
x: -1500,
|
||||
},
|
||||
visible: {
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 1,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
x: -1500,
|
||||
},
|
||||
};
|
||||
@@ -32,7 +32,7 @@ interface TranslationContextValue {
|
||||
getLocalizedString: (key: keyof typeof langEn, lang?: string) => string;
|
||||
}
|
||||
|
||||
export const TranslationContext = createContext<TranslationContextValue>({
|
||||
const TranslationContext = createContext<TranslationContextValue>({
|
||||
getLocalizedString: () => '',
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ interface SectionProps {
|
||||
|
||||
export default memo(Section);
|
||||
|
||||
export function Section(props: SectionProps) {
|
||||
function Section(props: SectionProps) {
|
||||
const { category, content, title, status } = props;
|
||||
|
||||
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
MILLIS_PER_HOUR,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { clamp } from '../../common/utils/math';
|
||||
import { formatDuration } from '../../common/utils/time';
|
||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||
|
||||
@@ -22,13 +21,6 @@ type CSSPosition = {
|
||||
width: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the position (in %) of an element relative to a schedule
|
||||
*/
|
||||
export function getRelativePositionX(scheduleStart: number, scheduleEnd: number, now: number): number {
|
||||
return clamp(((now - scheduleStart) / (scheduleEnd - scheduleStart)) * 100, 0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an absolute position of an element based on a schedule
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,7 @@ import { authenticateSocket } from '../middleware/authenticate.js';
|
||||
|
||||
let instance: SocketServer | null = null;
|
||||
|
||||
export class SocketServer implements IAdapter {
|
||||
class SocketServer implements IAdapter {
|
||||
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
|
||||
|
||||
private wss: WebSocketServer | null;
|
||||
|
||||
@@ -12,5 +12,3 @@ export const router = express.Router();
|
||||
router.post('/upload', uploadExcel, validateFileExists, postExcel);
|
||||
router.get('/worksheets', getWorksheets);
|
||||
router.post('/preview', validateImportMapOptions, previewExcel);
|
||||
|
||||
// TODO: validate import map
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
ErrorResponse,
|
||||
MessageResponse,
|
||||
OntimeRundown,
|
||||
OntimeRundownEntry,
|
||||
RundownCached,
|
||||
RundownPaginated,
|
||||
} from 'ontime-types';
|
||||
import { ErrorResponse, MessageResponse, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
@@ -21,12 +14,7 @@ import {
|
||||
reorderEvent,
|
||||
swapEvents,
|
||||
} from '../../services/rundown-service/RundownService.js';
|
||||
import {
|
||||
getEventWithId,
|
||||
getNormalisedRundown,
|
||||
getPaginated,
|
||||
getRundown,
|
||||
} from '../../services/rundown-service/rundownUtils.js';
|
||||
import { getEventWithId, getNormalisedRundown, getRundown } from '../../services/rundown-service/rundownUtils.js';
|
||||
|
||||
export async function rundownGetAll(_req: Request, res: Response<OntimeRundown>) {
|
||||
const rundown = getRundown();
|
||||
@@ -55,34 +43,6 @@ export async function rundownGetById(req: Request, res: Response<OntimeRundownEn
|
||||
}
|
||||
}
|
||||
|
||||
export async function rundownGetPaginated(req: Request, res: Response<RundownPaginated | ErrorResponse>) {
|
||||
const { limit, offset } = req.query;
|
||||
|
||||
if (limit == null && offset == null) {
|
||||
return res.json({
|
||||
rundown: getRundown(),
|
||||
total: getRundown().length,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let parsedOffset = Number(offset);
|
||||
if (Number.isNaN(parsedOffset)) {
|
||||
parsedOffset = 0;
|
||||
}
|
||||
let parsedLimit = Number(limit);
|
||||
if (Number.isNaN(parsedLimit)) {
|
||||
parsedLimit = Infinity;
|
||||
}
|
||||
const paginatedRundown = getPaginated(parsedOffset, parsedLimit);
|
||||
|
||||
res.status(200).json(paginatedRundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).json({ message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function rundownPost(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
rundownGetAll,
|
||||
rundownGetById,
|
||||
rundownGetNormalised,
|
||||
rundownGetPaginated,
|
||||
rundownPost,
|
||||
rundownPut,
|
||||
rundownReorder,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
paramsMustHaveEventId,
|
||||
rundownArrayOfIds,
|
||||
rundownBatchPutValidator,
|
||||
rundownGetPaginatedQueryParams,
|
||||
rundownPostValidator,
|
||||
rundownPutValidator,
|
||||
rundownReorderValidator,
|
||||
@@ -28,7 +26,6 @@ import {
|
||||
export const router = express.Router();
|
||||
|
||||
router.get('/', rundownGetAll); // not used in Ontime frontend
|
||||
router.get('/paginated', rundownGetPaginatedQueryParams, rundownGetPaginated); // not used in Ontime frontend
|
||||
router.get('/normalised', rundownGetNormalised);
|
||||
router.get('/:eventId', paramsMustHaveEventId, rundownGetById); // not used in Ontime frontend
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { body, param, query, validationResult } from 'express-validator';
|
||||
import { body, param, validationResult } from 'express-validator';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const rundownPostValidator = [
|
||||
@@ -77,14 +77,3 @@ export const rundownArrayOfIds = [
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
export const rundownGetPaginatedQueryParams = [
|
||||
query('offset').isNumeric().optional(),
|
||||
query('limit').isNumeric().optional(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { handleLegacyMessageConversion } from '../integration.legacy.js';
|
||||
|
||||
describe('handleLegacyConversion', () => {
|
||||
it('should return the payload as is if it is not a legacy message', () => {
|
||||
expect(handleLegacyMessageConversion({})).toEqual({});
|
||||
const newPayload = {
|
||||
timer: {
|
||||
text: 'text',
|
||||
visible: true,
|
||||
blink: true,
|
||||
blackout: true,
|
||||
},
|
||||
external: 'text',
|
||||
};
|
||||
expect(handleLegacyMessageConversion(newPayload)).toEqual(newPayload);
|
||||
});
|
||||
|
||||
it('should convert a legacy payload with external message', () => {
|
||||
expect(handleLegacyMessageConversion({ external: { text: 'text', visible: true } })).toEqual({
|
||||
external: 'text',
|
||||
timer: {
|
||||
secondarySource: 'external',
|
||||
},
|
||||
});
|
||||
|
||||
expect(handleLegacyMessageConversion({ external: { visible: true } })).toEqual({
|
||||
timer: {
|
||||
secondarySource: 'external',
|
||||
},
|
||||
});
|
||||
|
||||
expect(handleLegacyMessageConversion({ external: { text: 'text' } })).toEqual({
|
||||
external: 'text',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,6 @@ import { socket } from '../adapters/WebsocketAdapter.js';
|
||||
import { throttle } from '../utils/throttle.js';
|
||||
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
|
||||
|
||||
import { handleLegacyMessageConversion } from './integration.legacy.js';
|
||||
|
||||
const throttledUpdateEvent = throttle(updateEvent, 20);
|
||||
let lastRequest: Date | null = null;
|
||||
|
||||
@@ -89,12 +87,9 @@ const actionHandlers: Record<string, ActionHandler> = {
|
||||
message: (payload) => {
|
||||
assert.isObject(payload);
|
||||
|
||||
// TODO: remove this once we feel its been enough time, ontime 3.6.0, 20/09/2024
|
||||
const migratedPayload = handleLegacyMessageConversion(payload);
|
||||
|
||||
const patch: DeepPartial<MessageState> = {
|
||||
timer: 'timer' in migratedPayload ? validateTimerMessage(migratedPayload.timer) : undefined,
|
||||
external: 'external' in migratedPayload ? validateMessage(migratedPayload.external) : undefined,
|
||||
timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined,
|
||||
external: 'external' in payload ? validateMessage(payload.external) : undefined,
|
||||
};
|
||||
|
||||
const newMessage = messageService.patch(patch);
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { MessageState } from 'ontime-types';
|
||||
import { DeepPartial } from 'ts-essentials';
|
||||
|
||||
export type LegacyMessageState = DeepPartial<{
|
||||
timer: {
|
||||
text: string;
|
||||
visible: boolean;
|
||||
blink: boolean;
|
||||
blackout: boolean;
|
||||
};
|
||||
external: {
|
||||
text: string;
|
||||
visible: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
function isLegacyMessageState(value: object): value is LegacyMessageState {
|
||||
// @ts-expect-error -- good enough here
|
||||
return value?.external?.text !== undefined || value?.external?.visible !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to maintain support for legacy data in the /message endpoint
|
||||
* The previous message endpoint expected a patch of the message state
|
||||
* @example {
|
||||
* timer: { blink: boolean, blackout: boolean, text: string, visible: boolean },
|
||||
* external: { visible: boolean, text: string }
|
||||
* }
|
||||
*
|
||||
* This change is introduced in version 3.6.0
|
||||
*/
|
||||
export function handleLegacyMessageConversion(payload: object): object | Partial<MessageState> {
|
||||
// if it is not a legacy message, we pass it as is
|
||||
if (!isLegacyMessageState(payload)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current migration only needs to handle the cases
|
||||
* for the deprecated external message controls
|
||||
*/
|
||||
|
||||
// Migrate external message
|
||||
// 2.1 the user gives us the text and a visible flag
|
||||
if (payload?.external?.text !== undefined && payload.external.visible !== undefined) {
|
||||
return {
|
||||
timer: { secondarySource: payload.external.visible ? 'external' : null },
|
||||
external: payload.external.text,
|
||||
} as Partial<MessageState>;
|
||||
}
|
||||
// 2.2 the user gives us the text
|
||||
else if (payload?.external?.text !== undefined) {
|
||||
return {
|
||||
external: payload.external.text,
|
||||
} as Partial<MessageState>;
|
||||
}
|
||||
// 2.3 the user gives us the visible flag
|
||||
else if (payload?.external?.visible !== undefined) {
|
||||
return {
|
||||
timer: { secondarySource: payload.external.visible ? 'external' : null },
|
||||
} as Partial<MessageState>;
|
||||
}
|
||||
|
||||
// there should be no case for us to reach this since
|
||||
// the type guard would have ensured one of the above states
|
||||
return payload;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ export const startIntegrations = async () => {
|
||||
* @param {number} exitCode
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const shutdown = async (exitCode = 0) => {
|
||||
const shutdown = async (exitCode = 0) => {
|
||||
consoleHighlight(`Ontime shutting down with code ${exitCode}`);
|
||||
|
||||
// clear the restore file if it was a normal exit
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Log, LogLevel } from 'ontime-types';
|
||||
import { generateId, millisToString } from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||
import { consoleSubdued, consoleError } from '../utils/console.js';
|
||||
import { timeNow } from '../utils/time.js';
|
||||
import { isProduction } from '../externals.js';
|
||||
|
||||
class Logger {
|
||||
@@ -75,7 +75,7 @@ class Logger {
|
||||
level,
|
||||
origin,
|
||||
text,
|
||||
time: millisToString(clock.getSystemTime() || 0),
|
||||
time: millisToString(timeNow()),
|
||||
};
|
||||
this._push(log);
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
enum Source {
|
||||
System = 'system',
|
||||
MIDI = 'MIDI',
|
||||
}
|
||||
|
||||
/**
|
||||
* Service manages retrieving current time from a managed time source
|
||||
*/
|
||||
class Clock {
|
||||
private static instance: Clock;
|
||||
private readonly source: Source;
|
||||
|
||||
constructor(source?: Source) {
|
||||
if (Clock.instance) {
|
||||
return Clock.instance;
|
||||
}
|
||||
|
||||
Clock.instance = this;
|
||||
|
||||
this.source = source || Source.System;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time from source
|
||||
*/
|
||||
timeNow(): number {
|
||||
switch (this.source) {
|
||||
case Source.System:
|
||||
return this.getSystemTime();
|
||||
case Source.MIDI:
|
||||
// @ts-expect-error -- not implemented
|
||||
return this.getMidiTime();
|
||||
default:
|
||||
throw new Error('Invalid time source');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time from system
|
||||
*/
|
||||
getSystemTime() {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time from MIDI
|
||||
*/
|
||||
getMidiTime() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
export const clock = new Clock();
|
||||
@@ -4,8 +4,8 @@ import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
import { timerConfig } from '../../config/config.js';
|
||||
|
||||
export type EmitFn = (state: SimpleTimerState) => void;
|
||||
export type GetTimeFn = () => number;
|
||||
type EmitFn = (state: SimpleTimerState) => void;
|
||||
type GetTimeFn = () => number;
|
||||
|
||||
export class AuxTimerService {
|
||||
private timer: SimpleTimer;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
CustomFields,
|
||||
EndAction,
|
||||
EventCustomFields,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
@@ -8,11 +7,9 @@ import {
|
||||
OntimeRundown,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
TimerType,
|
||||
} from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils';
|
||||
|
||||
import { calculateRuntimeDelays, getDelayAt, calculateRuntimeDelaysFrom } from '../delayUtils.js';
|
||||
import {
|
||||
add,
|
||||
batchEdit,
|
||||
@@ -557,410 +554,6 @@ describe('swap() mutation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateRuntimeDelays', () => {
|
||||
it('calculates all delays in a given rundown', () => {
|
||||
const rundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
cue: '1',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
cue: '2',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
duration: 1200000,
|
||||
type: SupportedEvent.Delay,
|
||||
id: '7db42',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
cue: '3',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
cue: '4',
|
||||
custom: {},
|
||||
},
|
||||
];
|
||||
|
||||
const updatedRundown = calculateRuntimeDelays(rundown);
|
||||
|
||||
expect(rundown.length).toBe(updatedRundown.length);
|
||||
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
|
||||
expect((updatedRundown[2] as OntimeEvent).delay).toBe(600000);
|
||||
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
|
||||
expect((updatedRundown[6] as OntimeEvent).delay).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDelayAt()', () => {
|
||||
const delayedRundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
cue: '1',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
delay: 600000,
|
||||
cue: '2',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
duration: 1200000,
|
||||
type: SupportedEvent.Delay,
|
||||
id: '7db42',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
delay: 1800000,
|
||||
cue: '3',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
delay: 0,
|
||||
cue: '4',
|
||||
custom: {},
|
||||
},
|
||||
];
|
||||
|
||||
it('calculates delay in a rundown', () => {
|
||||
const delayAtStart = getDelayAt(0, delayedRundown);
|
||||
const delayOnFirstEvent = getDelayAt(2, delayedRundown);
|
||||
const delayOnSecondEvent = getDelayAt(4, delayedRundown);
|
||||
const delayOnBlockedEvent = getDelayAt(0, delayedRundown);
|
||||
|
||||
expect(delayAtStart).toBe(0);
|
||||
expect(delayOnFirstEvent).toBe(600000);
|
||||
expect(delayOnSecondEvent).toBe(600000 + 1200000);
|
||||
expect(delayOnBlockedEvent).toBe(0);
|
||||
});
|
||||
it('finds delay before a delay block', () => {
|
||||
const valueOnFirstDelayBlock = getDelayAt(1, delayedRundown);
|
||||
const valueOnSecondDelayBlock = getDelayAt(3, delayedRundown);
|
||||
const valueAfterSecondDelayBlock = getDelayAt(4, delayedRundown);
|
||||
|
||||
expect(valueOnFirstDelayBlock).toBe(0);
|
||||
expect(valueOnSecondDelayBlock).toBe(600000);
|
||||
expect(valueAfterSecondDelayBlock).toBe(600000 + 1200000);
|
||||
});
|
||||
it('returns 0 after blocks', () => {
|
||||
const valueOnBlock = getDelayAt(6, delayedRundown);
|
||||
expect(valueOnBlock).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateRuntimeDelaysFrom()', () => {
|
||||
it('updates delays from given id', () => {
|
||||
const delayedRundown: OntimeRundown = [
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
delay: 0,
|
||||
cue: '1',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
duration: 600000,
|
||||
type: SupportedEvent.Delay,
|
||||
id: '07986',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1200000,
|
||||
duration: 0,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
delay: 0,
|
||||
cue: '2',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
duration: 1200000,
|
||||
type: SupportedEvent.Delay,
|
||||
id: '7db42',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 600000,
|
||||
timeEnd: 1200000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
delay: 1800000,
|
||||
cue: '3',
|
||||
custom: {},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
type: SupportedEvent.Block,
|
||||
id: '9870d',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
timeStart: 1200000,
|
||||
timeEnd: 1800000,
|
||||
duration: 600000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
delay: 0,
|
||||
cue: '4',
|
||||
custom: {},
|
||||
},
|
||||
];
|
||||
|
||||
const updatedRundown = calculateRuntimeDelaysFrom('07986', delayedRundown);
|
||||
|
||||
// we only update from the 4th on
|
||||
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
|
||||
// 1 + 3
|
||||
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom fields', () => {
|
||||
describe('createCustomField()', () => {
|
||||
it('creates a field from given parameters', () => {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { OntimeRundown } from 'ontime-types';
|
||||
import { getPaginated } from '../rundownUtils.js';
|
||||
|
||||
describe('getPaginated', () => {
|
||||
// mock cache so we dont run data functions
|
||||
beforeAll(() => {
|
||||
vi.mock('../rundownCache.js', () => ({}));
|
||||
});
|
||||
|
||||
// @ts-expect-error -- we know this is not correct, but good enough for the test
|
||||
const getData = () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as OntimeRundown;
|
||||
|
||||
it('should return the correct paginated rundown', () => {
|
||||
const offset = 0;
|
||||
const limit = 1;
|
||||
const result = getPaginated(offset, limit, getData);
|
||||
|
||||
expect(result.rundown).toHaveLength(1);
|
||||
expect(result.total).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle overflows', () => {
|
||||
const offset = 0;
|
||||
const limit = 20;
|
||||
const result = getPaginated(offset, limit, getData);
|
||||
|
||||
expect(result.rundown).toHaveLength(10);
|
||||
expect(result.total).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle out of range', () => {
|
||||
const offset = 11;
|
||||
const limit = Infinity;
|
||||
const result = getPaginated(offset, limit, getData);
|
||||
|
||||
expect(result.rundown).toHaveLength(0);
|
||||
expect(result.total).toBe(10);
|
||||
});
|
||||
});
|
||||
@@ -1,94 +1,6 @@
|
||||
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
import { OntimeRundown, isOntimeDelay, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
import { deleteAtIndex } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* Calculates all delays in a given rundown
|
||||
* @param rundown
|
||||
*/
|
||||
export function calculateRuntimeDelays(rundown: OntimeRundown) {
|
||||
let accumulatedDelay = 0;
|
||||
const updatedRundown = [...rundown];
|
||||
|
||||
for (const [index, event] of updatedRundown.entries()) {
|
||||
if (isOntimeDelay(event)) {
|
||||
accumulatedDelay += event.duration;
|
||||
} else if (isOntimeBlock(event)) {
|
||||
accumulatedDelay = 0;
|
||||
} else if (isOntimeEvent(event)) {
|
||||
updatedRundown[index] = {
|
||||
...event,
|
||||
delay: accumulatedDelay,
|
||||
};
|
||||
}
|
||||
}
|
||||
return updatedRundown;
|
||||
}
|
||||
/**
|
||||
* Calculate delays in rundown from a given index
|
||||
* @param eventIndex
|
||||
* @param rundown
|
||||
*/
|
||||
export function calculateRuntimeDelaysFromIndex(eventIndex: number, rundown: OntimeRundown) {
|
||||
if (eventIndex === -1) {
|
||||
throw new Error('ID not found at index');
|
||||
}
|
||||
|
||||
let accumulatedDelay = getDelayAt(eventIndex, rundown);
|
||||
const updatedRundown = [...rundown];
|
||||
|
||||
for (let i = eventIndex; i < rundown.length; i++) {
|
||||
const event = rundown[i];
|
||||
if (isOntimeDelay(event)) {
|
||||
accumulatedDelay += event.duration;
|
||||
} else if (isOntimeBlock(event)) {
|
||||
if (i === eventIndex) {
|
||||
accumulatedDelay = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (isOntimeEvent(event)) {
|
||||
updatedRundown[i] = {
|
||||
...event,
|
||||
delay: accumulatedDelay,
|
||||
};
|
||||
}
|
||||
}
|
||||
return updatedRundown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate delays in rundown from an event with given id
|
||||
* @param eventId
|
||||
* @param rundown
|
||||
*/
|
||||
export function calculateRuntimeDelaysFrom(eventId: string, rundown: OntimeRundown) {
|
||||
const index = rundown.findIndex((event) => event.id === eventId);
|
||||
return calculateRuntimeDelaysFromIndex(index, rundown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates delay to an event at a given index
|
||||
* @param eventIndex
|
||||
* @param rundown
|
||||
*/
|
||||
export function getDelayAt(eventIndex: number, rundown: OntimeRundown): number {
|
||||
if (eventIndex < 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// we need to check the event before
|
||||
const event = rundown[eventIndex - 1];
|
||||
|
||||
if (isOntimeDelay(event)) {
|
||||
return event.duration + getDelayAt(eventIndex - 1, rundown);
|
||||
} else if (isOntimeBlock(event)) {
|
||||
return 0;
|
||||
} else if (isOntimeEvent(event)) {
|
||||
return event.delay ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies delay from given event ID, deletes the delay event after
|
||||
* @throws {Error} if event ID not found or is not a delay
|
||||
|
||||
@@ -105,7 +105,7 @@ export function handleCustomField(
|
||||
}
|
||||
|
||||
/** List of event properties which do not need the rundown to be regenerated */
|
||||
export enum regenerateWhitelist {
|
||||
enum RegenerateWhitelist {
|
||||
'id',
|
||||
'cue',
|
||||
'title',
|
||||
@@ -125,7 +125,7 @@ export enum regenerateWhitelist {
|
||||
* @param path
|
||||
*/
|
||||
export function isDataStale(patch: Partial<OntimeRundownEntry>): boolean {
|
||||
return Object.keys(patch).some((key) => !(key in regenerateWhitelist));
|
||||
return Object.keys(patch).some((key) => !(key in RegenerateWhitelist));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +133,7 @@ export function isDataStale(patch: Partial<OntimeRundownEntry>): boolean {
|
||||
* @param path
|
||||
*/
|
||||
export function willCauseRegeneration(key: keyof OntimeEvent): boolean {
|
||||
return !(key in regenerateWhitelist);
|
||||
return !(key in RegenerateWhitelist);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,19 +101,3 @@ export function findNext(currentEventId?: string): PlayableEvent | null {
|
||||
const nextEvent = playableEvents.at(newIndex);
|
||||
return nextEvent ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a paginated rundown
|
||||
* Exposes a getter function for the rundown for testing
|
||||
*/
|
||||
export function getPaginated(
|
||||
offset: number,
|
||||
limit: number,
|
||||
source = getRundown,
|
||||
): { rundown: OntimeRundownEntry[]; total: number } {
|
||||
const rundown = source();
|
||||
return {
|
||||
rundown: rundown.slice(Math.min(offset, rundown.length), Math.min(offset + limit, rundown.length)),
|
||||
total: rundown.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
isPlaybackActive,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { timeNow } from '../utils/time.js';
|
||||
import type { RestorePoint } from '../services/RestoreService.js';
|
||||
import {
|
||||
getCurrent,
|
||||
@@ -52,7 +52,7 @@ export type RuntimeState = {
|
||||
};
|
||||
|
||||
const runtimeState: RuntimeState = {
|
||||
clock: clock.timeNow(),
|
||||
clock: timeNow(),
|
||||
currentBlock: { ...runtimeStorePlaceholder.currentBlock },
|
||||
eventNow: null,
|
||||
publicEventNow: null,
|
||||
@@ -98,7 +98,7 @@ export function clear() {
|
||||
runtimeState.runtime.selectedEventIndex = null;
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = clock.timeNow();
|
||||
runtimeState.clock = timeNow();
|
||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||
|
||||
// when clearing, we maintain the total delay from the rundown
|
||||
@@ -357,7 +357,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.timer.playback === Playback.Play) {
|
||||
return false;
|
||||
}
|
||||
state.clock = clock.timeNow();
|
||||
state.clock = timeNow();
|
||||
state.timer.secondaryTimer = null;
|
||||
|
||||
// add paused time if it exists
|
||||
@@ -400,7 +400,7 @@ export function pause(state: RuntimeState = runtimeState): boolean {
|
||||
}
|
||||
|
||||
state.timer.playback = Playback.Pause;
|
||||
state.clock = clock.timeNow();
|
||||
state.clock = timeNow();
|
||||
state._timer.pausedAt = state.clock;
|
||||
return true;
|
||||
}
|
||||
@@ -438,7 +438,7 @@ export function addTime(amount: number) {
|
||||
|
||||
if (willGoNegative && !hasFinished) {
|
||||
// set finished time so side effects are triggered
|
||||
runtimeState._timer.forceFinish = clock.timeNow();
|
||||
runtimeState._timer.forceFinish = timeNow();
|
||||
} else {
|
||||
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
|
||||
if (willGoPositive) {
|
||||
@@ -466,7 +466,7 @@ export type UpdateResult = {
|
||||
export function update(): UpdateResult {
|
||||
// 0. there are some things we always do
|
||||
const previousClock = runtimeState.clock;
|
||||
runtimeState.clock = clock.timeNow(); // we update the clock on every update call
|
||||
runtimeState.clock = timeNow(); // we update the clock on every update call
|
||||
|
||||
// 1. is playback idle?
|
||||
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
CustomFields,
|
||||
DatabaseModel,
|
||||
EndAction,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
Settings,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
TimerType,
|
||||
URLPreset,
|
||||
} from 'ontime-types';
|
||||
|
||||
@@ -360,46 +357,3 @@ describe('parseRundown() linking', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseRundown() migrations', () => {
|
||||
const legacyEvent = {
|
||||
id: '1',
|
||||
type: SupportedEvent.Event,
|
||||
cue: '',
|
||||
title: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: 'time-to-end',
|
||||
linkStart: null,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
duration: 0,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: '',
|
||||
revision: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
custom: {},
|
||||
};
|
||||
|
||||
it('migrates an event with time-to-end', () => {
|
||||
const result = parseRundown({ rundown: [legacyEvent] as OntimeRundown });
|
||||
expect(result.rundown[0]).toMatchObject({
|
||||
id: '1',
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('migrates an event without time-to-end', () => {
|
||||
const countdownEvent = { ...legacyEvent, timerType: TimerType.CountDown };
|
||||
const result = parseRundown({ rundown: [countdownEvent] as OntimeRundown });
|
||||
expect(result.rundown[0]).toMatchObject({
|
||||
id: '1',
|
||||
timerType: TimerType.CountDown,
|
||||
countToEnd: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -303,7 +303,7 @@ export const parseExcel = (
|
||||
};
|
||||
};
|
||||
|
||||
export type ParsingError = {
|
||||
type ParsingError = {
|
||||
context: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
OntimeRundown,
|
||||
ProjectData,
|
||||
Settings,
|
||||
TimerType,
|
||||
URLPreset,
|
||||
ViewSettings,
|
||||
isOntimeBlock,
|
||||
@@ -53,7 +52,7 @@ export function parseRundown(
|
||||
let newEvent: OntimeEvent | OntimeDelay | OntimeBlock | null;
|
||||
|
||||
if (isOntimeEvent(event)) {
|
||||
const maybeEvent = runEventMigrations({ ...event, id });
|
||||
const maybeEvent = { ...event, id };
|
||||
|
||||
if (event.linkStart) {
|
||||
maybeEvent.linkStart = previousId;
|
||||
@@ -246,22 +245,3 @@ export function sanitiseCustomFields(data: object): CustomFields {
|
||||
|
||||
return newCustomFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time to end was moved from a TimerType to a standalone boolean named count to end
|
||||
* Released as part of v3.10.0
|
||||
*/
|
||||
function migrateTimeToEnd(event: any): OntimeEvent {
|
||||
if (event.timerType === 'time-to-end') {
|
||||
event.timerType = TimerType.CountDown;
|
||||
event.countToEnd = true;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutating function migrates event data entries
|
||||
*/
|
||||
function runEventMigrations(event: any): OntimeEvent {
|
||||
return migrateTimeToEnd(event);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { parse } from 'path';
|
||||
|
||||
/**
|
||||
* @description Takes a filename and removes the extension
|
||||
* @param {string} filename - filename with extension
|
||||
*/
|
||||
export const removeFileExtension = (filename: string): string => {
|
||||
return parse(filename).name;
|
||||
};
|
||||
@@ -60,3 +60,17 @@ export function getTimezoneLabel(date: Date): string {
|
||||
|
||||
return `GMT ${sign}${pad(hours)}:${pad(minutes)} ${tzName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time from system
|
||||
*/
|
||||
export function timeNow() {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { OntimeRundown } from '../../definitions/core/Rundown.type.js';
|
||||
import type { Playback } from '../../definitions/runtime/Playback.type.js';
|
||||
import type { MaybeString } from '../../utils/utils.type.js';
|
||||
|
||||
@@ -51,8 +50,3 @@ export type ProjectLogoResponse = {
|
||||
export type ErrorResponse = MessageResponse;
|
||||
|
||||
export type AuthenticationStatus = 'authenticated' | 'not_authenticated' | 'pending';
|
||||
|
||||
export type RundownPaginated = {
|
||||
rundown: OntimeRundown;
|
||||
total: number;
|
||||
};
|
||||
|
||||
@@ -67,7 +67,6 @@ export type {
|
||||
ErrorResponse,
|
||||
ProjectFileListResponse,
|
||||
MessageResponse,
|
||||
RundownPaginated,
|
||||
SessionStats,
|
||||
ProjectLogoResponse,
|
||||
} from './api/ontime-controller/BackendResponse.type.js';
|
||||
|
||||
Reference in New Issue
Block a user