mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 832457620e | |||
| 9feec76858 | |||
| 05e3c8a9f8 | |||
| 08c580f773 | |||
| c33cbbea7d | |||
| 7c642f3832 | |||
| 729c68e9e2 | |||
| 932bb8102c | |||
| ee39bc296f | |||
| 8cda9eac0a | |||
| 140a3310cb |
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myCustomPropertiesEnabled" value="true" />
|
||||
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
||||
<option name="myCustomPropertiesList">
|
||||
<value>
|
||||
<list size="3">
|
||||
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
|
||||
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
|
||||
<item index="2" class="java.lang.String" itemvalue="app-region" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredUrls">
|
||||
<list>
|
||||
<option value="http://localhost" />
|
||||
<option value="http://127.0.0.1" />
|
||||
<option value="http://0.0.0.0" />
|
||||
<option value="http://www.w3.org/" />
|
||||
<option value="http://json-schema.org/draft" />
|
||||
<option value="http://java.sun.com/" />
|
||||
<option value="http://xmlns.jcp.org/" />
|
||||
<option value="http://javafx.com/javafx/" />
|
||||
<option value="http://javafx.com/fxml" />
|
||||
<option value="http://maven.apache.org/xsd/" />
|
||||
<option value="http://maven.apache.org/POM/" />
|
||||
<option value="http://www.springframework.org/schema/" />
|
||||
<option value="http://www.springframework.org/tags" />
|
||||
<option value="http://www.springframework.org/security/tags" />
|
||||
<option value="http://www.thymeleaf.org" />
|
||||
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||
<option value="http://www.jboss.com/xml/ns/" />
|
||||
<option value="http://www.ibm.com/webservices/xsd" />
|
||||
<option value="http://activemq.apache.org/schema/" />
|
||||
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||
<option value="http://schemas.xmlsoap.org/" />
|
||||
<option value="http://cxf.apache.org/schemas/" />
|
||||
<option value="http://primefaces.org/ui" />
|
||||
<option value="http://tiles.apache.org/" />
|
||||
<option value="http://__IP__" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "3.14.3",
|
||||
"version": "3.14.2",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "3.14.3",
|
||||
"version": "3.14.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -13,6 +13,7 @@
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@fontsource/open-sans": "^5.0.28",
|
||||
"@mantine/hooks": "^7.13.3",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@sentry/react": "^8.43.0",
|
||||
"@table-nav/react": "^0.0.7",
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
@@ -28,7 +29,6 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react-icons": "5.4.0",
|
||||
"react-qr-code": "^2.0.12",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"web-vitals": "^3.1.1",
|
||||
|
||||
@@ -10,6 +10,7 @@ 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'];
|
||||
@@ -18,8 +19,11 @@ export const REPORT = ['report'];
|
||||
// API URLs
|
||||
export const apiEntryUrl = `${serverURL}/data`;
|
||||
|
||||
const userAssetsPath = 'user';
|
||||
const cssOverridePath = 'styles/override.css';
|
||||
export const projectDataURL = `${serverURL}/project`;
|
||||
export const rundownURL = `${serverURL}/events`;
|
||||
export const ontimeURL = `${serverURL}/ontime`;
|
||||
|
||||
export const userAssetsPath = 'user';
|
||||
export const cssOverridePath = 'styles/override.css';
|
||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||
|
||||
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||
import { makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import { createBlob, downloadBlob } from './utils';
|
||||
@@ -40,11 +40,9 @@ export async function downloadProject(fileName: string) {
|
||||
export async function downloadCSV(fileName: string = 'rundown') {
|
||||
try {
|
||||
const { data, name } = await fileDownload(fileName);
|
||||
const { project, rundowns, customFields } = data;
|
||||
|
||||
const flatRundowns = aggregateRundowns(rundowns);
|
||||
const sheetData = makeTable(project, flatRundowns, customFields);
|
||||
const { project, rundown, customFields } = data;
|
||||
|
||||
const sheetData = makeTable(project, rundown, customFields);
|
||||
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
||||
|
||||
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { CustomFields, Rundown } from 'ontime-types';
|
||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
const excelPath = `${apiEntryUrl}/excel`;
|
||||
|
||||
type PreviewSpreadsheetResponse = {
|
||||
rundown: OntimeRundown;
|
||||
customFields: CustomFields;
|
||||
};
|
||||
|
||||
/**
|
||||
* upload Excel file to server
|
||||
* @return string - file ID op the uploaded file
|
||||
@@ -29,10 +34,6 @@ export async function getWorksheetNames(): Promise<string[]> {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
type PreviewSpreadsheetResponse = {
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
};
|
||||
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||
options,
|
||||
|
||||
@@ -1,44 +1,29 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import {
|
||||
MessageResponse,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
ProjectRundownsList,
|
||||
Rundown,
|
||||
TransientEventPayload,
|
||||
} from 'ontime-types';
|
||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
const rundownPath = `${apiEntryUrl}/rundown`;
|
||||
|
||||
/**
|
||||
* HTTP request to fetch a list of existing rundowns
|
||||
*/
|
||||
export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
||||
const res = await axios.get(`${rundownPath}/`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to fetch all events
|
||||
*/
|
||||
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||
const res = await axios.get(`${rundownPath}/current`);
|
||||
export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
||||
const res = await axios.get(`${rundownPath}/normalised`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to post new event
|
||||
*/
|
||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeEntry>> {
|
||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
return axios.post(rundownPath, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to put new event
|
||||
*/
|
||||
export async function requestPutEvent(data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
||||
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
return axios.put(rundownPath, data);
|
||||
}
|
||||
|
||||
@@ -63,7 +48,7 @@ export type ReorderEntry = {
|
||||
/**
|
||||
* HTTP request to reorder events
|
||||
*/
|
||||
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeEntry>> {
|
||||
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
return axios.patch(`${rundownPath}/reorder`, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
@@ -54,7 +54,7 @@ export const previewRundown = async (
|
||||
sheetId: string,
|
||||
options: ImportMap,
|
||||
): Promise<{
|
||||
rundown: Rundown;
|
||||
rundown: OntimeRundown;
|
||||
customFields: CustomFields;
|
||||
}> => {
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { IoArrowForward } from 'react-icons/io5';
|
||||
import {
|
||||
IconButton,
|
||||
Input,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
ModalOverlay,
|
||||
Select,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoArrowForward } from '@react-icons/all-files/io5/IoArrowForward';
|
||||
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
||||
import { IconType } from '@react-icons/all-files';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { ContextMenuOption } from './ContextMenuOption';
|
||||
@@ -23,7 +23,7 @@ export type OptionWithoutGroup = {
|
||||
withDivider?: boolean;
|
||||
};
|
||||
|
||||
type OptionWithGroup = {
|
||||
export type OptionWithGroup = {
|
||||
label: string;
|
||||
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { IoCopy } from 'react-icons/io5';
|
||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||
import { IoCopy } from '@react-icons/all-files/io5/IoCopy';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
import { Size } from '../../models/Util.type';
|
||||
import copyToClipboard from '../../utils/copyToClipboard';
|
||||
|
||||
interface CopyTagProps {
|
||||
copyValue: string;
|
||||
label: string;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
size?: Size;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IoChevronDown, IoChevronUp } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
|
||||
import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
import { millisToDelayString } from '../../utils/dateConfig';
|
||||
|
||||
@@ -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 { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
|
||||
import { runtimeStore } from '../../stores/runtime';
|
||||
import { runtimeStore } from '@/common/stores/runtime';
|
||||
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
||||
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
|
||||
@@ -37,7 +37,7 @@ class ErrorBoundary extends React.Component {
|
||||
scope.setExtras({
|
||||
error,
|
||||
store: appState,
|
||||
hasSocket: { hasConnected, reconnectAttempts },
|
||||
hasSocket: { hasConnected, shouldReconnect, reconnectAttempts },
|
||||
});
|
||||
const eventId = Sentry.captureException(error);
|
||||
this.setState({ eventId, info });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { IoAlertCircle } from 'react-icons/io5';
|
||||
import { IoAlertCircle } from '@react-icons/all-files/io5/IoAlertCircle';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IoBan } from 'react-icons/io5';
|
||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useController, UseControllerProps } from 'react-hook-form';
|
||||
import { IoEyedrop } from 'react-icons/io5';
|
||||
import { IoEyedrop } from '@react-icons/all-files/io5/IoEyedrop';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { debounce } from '../../../utils/debounce';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { IoOpenOutline } from 'react-icons/io5';
|
||||
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
|
||||
|
||||
import { openLink } from '../../../utils/linkUtils';
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import style from './LoaderOverlay.module.scss';
|
||||
|
||||
export default function LoaderOverlay() {
|
||||
return (
|
||||
<div className={style.overlay}>
|
||||
<span className={style.loader} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IoApps } from 'react-icons/io5';
|
||||
import { IoSettingsOutline } from 'react-icons/io5';
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { IoArrowUp, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Drawer,
|
||||
@@ -12,6 +11,11 @@ import {
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useFullscreen } from '@mantine/hooks';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
|
||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
|
||||
import { isLocalhost } from '../../../externals';
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IoLockClosedOutline } from 'react-icons/io5';
|
||||
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IoPause, IoPlay, IoStop } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { Playback } from 'ontime-types';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||
|
||||
import style from './ProtectRoute.module.scss';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { IoChevronDown } from 'react-icons/io5';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
Select,
|
||||
Switch,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
|
||||
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IoChevronDown } from 'react-icons/io5';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ type OptionsField = {
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
type MultiselectOption = { value: string; label: string; colour: string };
|
||||
export type MultiselectOption = { value: string; label: string; colour: string };
|
||||
export type MultiselectOptions = Record<string, MultiselectOption>;
|
||||
type MultiOptionsField = {
|
||||
type: 'multi-option';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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,9 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { getAutomationSettings } from '../api/automation';
|
||||
import { editAutomationSettings, 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({
|
||||
@@ -18,3 +20,15 @@ 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: '',
|
||||
};
|
||||
|
||||
function useProjectList() {
|
||||
export function useProjectList() {
|
||||
const { data, status, refetch } = useQuery({
|
||||
queryKey: PROJECT_LIST,
|
||||
queryFn: getProjects,
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { OntimeEntry, Rundown } from 'ontime-types';
|
||||
import { NormalisedRundown, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
import { fetchCurrentRundown } from '../api/rundown';
|
||||
import { fetchNormalisedRundown } from '../api/rundown';
|
||||
|
||||
import useProjectData from './useProjectData';
|
||||
|
||||
// revision is -1 so that the remote revision is higher
|
||||
const cachedRundownPlaceholder: Rundown = {
|
||||
id: 'default',
|
||||
title: '',
|
||||
order: [],
|
||||
entries: {},
|
||||
revision: -1,
|
||||
};
|
||||
const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 };
|
||||
|
||||
/**
|
||||
* Normalised rundown data
|
||||
*/
|
||||
export default function useRundown() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
||||
queryKey: RUNDOWN,
|
||||
queryFn: fetchCurrentRundown,
|
||||
queryFn: fetchNormalisedRundown,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
@@ -43,16 +37,16 @@ export function useFlatRundown() {
|
||||
|
||||
const loadedProject = useRef<string>('');
|
||||
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
||||
const [flatRundown, setFlatRundown] = useState<OntimeEntry[]>([]);
|
||||
const [flatRunDown, setFlatRunDown] = useState<OntimeRundown>([]);
|
||||
|
||||
// update data whenever the revision changes
|
||||
useEffect(() => {
|
||||
if (data.revision !== -1 && data.revision !== prevRevision) {
|
||||
const flatRundown = data.order.map((id) => data.entries[id]);
|
||||
setFlatRundown(flatRundown);
|
||||
const flatRundown = data.order.map((id) => data.rundown[id]);
|
||||
setFlatRunDown(flatRundown);
|
||||
setPrevRevision(data.revision);
|
||||
}
|
||||
}, [data.entries, data.order, data.revision, prevRevision]);
|
||||
}, [data.order, data.revision, data.rundown, prevRevision]);
|
||||
|
||||
// TODO: should we have a project id field?
|
||||
// invalidate current version if project changes
|
||||
@@ -63,13 +57,13 @@ export function useFlatRundown() {
|
||||
}
|
||||
}, [projectData]);
|
||||
|
||||
return { data: flatRundown, status };
|
||||
return { data: flatRunDown, status };
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to a partial rundown based on a filter callback
|
||||
*/
|
||||
export function usePartialRundown(cb: (event: OntimeEntry) => boolean) {
|
||||
export function usePartialRundown(cb: (event: OntimeRundownEntry) => boolean) {
|
||||
const { data, status } = useFlatRundown();
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter(cb);
|
||||
|
||||
@@ -2,12 +2,11 @@ import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
isOntimeEvent,
|
||||
MaybeString,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
Rundown,
|
||||
OntimeRundownEntry,
|
||||
RundownCached,
|
||||
TimeField,
|
||||
TimeStrategy,
|
||||
TransientEventPayload,
|
||||
@@ -32,12 +31,12 @@ import { useEditorSettings } from '../stores/editorSettings';
|
||||
|
||||
export type EventOptions = Partial<{
|
||||
// options to any new block (event / delay / block)
|
||||
after: MaybeString;
|
||||
before: MaybeString;
|
||||
after: string;
|
||||
before: string;
|
||||
// options to blocks of type OntimeEvent
|
||||
defaultPublic: boolean;
|
||||
linkPrevious: boolean;
|
||||
lastEventId: MaybeString;
|
||||
lastEventId: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
@@ -58,11 +57,11 @@ export const useEventAction = () => {
|
||||
|
||||
const getEventById = useCallback(
|
||||
(eventId: string) => {
|
||||
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
if (!cachedRundown?.entries) {
|
||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
if (!cachedRundown?.rundown) {
|
||||
return;
|
||||
}
|
||||
return cachedRundown.entries[eventId];
|
||||
return cachedRundown.rundown[eventId];
|
||||
},
|
||||
[queryClient],
|
||||
);
|
||||
@@ -101,8 +100,9 @@ export const useEventAction = () => {
|
||||
newEvent.linkStart = applicationOptions.lastEventId;
|
||||
} else if (applicationOptions?.lastEventId) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this is a value
|
||||
const rundownData = queryClient.getQueryData<Rundown>(RUNDOWN)!;
|
||||
const previousEvent = rundownData.entries[applicationOptions.lastEventId];
|
||||
const rundownData = queryClient.getQueryData<RundownCached>(RUNDOWN)!;
|
||||
const { rundown } = rundownData;
|
||||
const previousEvent = rundown[applicationOptions.lastEventId];
|
||||
if (isOntimeEvent(previousEvent)) {
|
||||
newEvent.timeStart = previousEvent.timeEnd;
|
||||
}
|
||||
@@ -180,21 +180,15 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
const eventId = newEvent.id;
|
||||
|
||||
if (previousData && eventId) {
|
||||
// optimistically update object
|
||||
const newRundown = { ...previousData.entries };
|
||||
const newRundown = { ...previousData.rundown };
|
||||
// @ts-expect-error -- we expect the events to be of same type
|
||||
newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: previousData.order,
|
||||
entries: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||
}
|
||||
|
||||
// Return a context with the previous and new events
|
||||
@@ -202,7 +196,7 @@ export const useEventAction = () => {
|
||||
},
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _newEvent, context) => {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -216,7 +210,7 @@ export const useEventAction = () => {
|
||||
* Updates existing event
|
||||
*/
|
||||
const updateEvent = useCallback(
|
||||
async (event: Partial<OntimeEntry>) => {
|
||||
async (event: Partial<OntimeRundownEntry>) => {
|
||||
try {
|
||||
await _updateEventMutation.mutateAsync(event);
|
||||
} catch (error) {
|
||||
@@ -302,9 +296,9 @@ export const useEventAction = () => {
|
||||
* Utility function to get the previous event end time
|
||||
*/
|
||||
function getPreviousEnd(): number {
|
||||
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
|
||||
if (!cachedRundown?.order || !cachedRundown?.entries) {
|
||||
if (!cachedRundown?.order || !cachedRundown?.rundown) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -314,7 +308,7 @@ export const useEventAction = () => {
|
||||
}
|
||||
let previousEnd = 0;
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const event = cachedRundown.entries[cachedRundown.order[i]];
|
||||
const event = cachedRundown.rundown[cachedRundown.order[i]];
|
||||
if (isOntimeEvent(event)) {
|
||||
previousEnd = event.timeEnd;
|
||||
break;
|
||||
@@ -337,11 +331,11 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousEvents = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
|
||||
if (previousRundown) {
|
||||
if (previousEvents) {
|
||||
const eventIds = new Set(ids);
|
||||
const newRundown = { ...previousRundown.entries };
|
||||
const newRundown = { ...previousEvents.rundown };
|
||||
|
||||
eventIds.forEach((eventId) => {
|
||||
if (Object.hasOwn(newRundown, eventId)) {
|
||||
@@ -355,22 +349,16 @@ export const useEventAction = () => {
|
||||
}
|
||||
});
|
||||
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousRundown.id,
|
||||
title: previousRundown.title,
|
||||
order: previousRundown.order,
|
||||
entries: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
queryClient.setQueryData(RUNDOWN, { order: previousEvents.order, rundown: newRundown, revision: -1 });
|
||||
}
|
||||
// Return a context with the previous rundown
|
||||
return { previousRundown };
|
||||
// Return a context with the previous and new events
|
||||
return { previousEvents };
|
||||
},
|
||||
onSettled: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
},
|
||||
onError: (_error, _newEvent, context) => {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousRundown);
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousEvents);
|
||||
},
|
||||
networkMode: 'always',
|
||||
});
|
||||
@@ -398,21 +386,19 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const newOrder = previousData.order.filter((id) => !eventIds.includes(id));
|
||||
const newRundown = { ...previousData.entries };
|
||||
const newRundown = { ...previousData.rundown };
|
||||
for (const eventId of eventIds) {
|
||||
delete newRundown[eventId];
|
||||
}
|
||||
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
queryClient.setQueryData(RUNDOWN, {
|
||||
order: newOrder,
|
||||
entries: newRundown,
|
||||
rundown: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
}
|
||||
@@ -423,7 +409,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -459,16 +445,10 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData?.id ?? 'default',
|
||||
title: previousData?.title ?? '',
|
||||
entries: {},
|
||||
order: [],
|
||||
revision: -1,
|
||||
});
|
||||
queryClient.setQueryData(RUNDOWN, { rundown: {}, order: [], revision: -1 });
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousData };
|
||||
@@ -476,7 +456,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -536,18 +516,13 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const newOrder = reorderArray(previousData.order, data.from, data.to);
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: newOrder,
|
||||
entries: previousData.entries,
|
||||
revision: -1,
|
||||
});
|
||||
|
||||
queryClient.setQueryData(RUNDOWN, { order: newOrder, rundown: previousData.rundown, revision: -1 });
|
||||
}
|
||||
|
||||
// Return a context with the previous and new events
|
||||
@@ -556,7 +531,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -597,28 +572,22 @@ export const useEventAction = () => {
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const newRundown = { ...previousData.entries };
|
||||
const eventA = previousData.entries[from];
|
||||
const eventB = previousData.entries[to];
|
||||
const newRundown = { ...previousData.rundown };
|
||||
const eventA = previousData.rundown[from];
|
||||
const eventB = previousData.rundown[to];
|
||||
|
||||
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [newA, newB] = swapEventData(eventA, eventB);
|
||||
const { newA, newB } = swapEventData(eventA, eventB);
|
||||
newRundown[from] = newA;
|
||||
newRundown[to] = newB;
|
||||
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: previousData.order,
|
||||
entries: newRundown,
|
||||
revision: -1,
|
||||
});
|
||||
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||
}
|
||||
|
||||
// Return a context with the previous events
|
||||
@@ -627,7 +596,7 @@ export const useEventAction = () => {
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _eventId, context) => {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
|
||||
@@ -12,7 +12,7 @@ type noop = (this: any, ...args: any[]) => any;
|
||||
|
||||
type PickFunction<T extends noop> = (this: ThisParameterType<T>, ...args: Parameters<T>) => ReturnType<T>;
|
||||
|
||||
const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
|
||||
export const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
|
||||
|
||||
export default function useMemoisedFn<T extends noop>(fn: T) {
|
||||
if (isDev) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
|
||||
import { OffsetMode, RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
|
||||
|
||||
import { useRuntimeStore } from '../stores/runtime';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
@@ -91,6 +91,14 @@ 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) => ({
|
||||
@@ -137,6 +145,8 @@ 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,
|
||||
@@ -150,7 +160,7 @@ export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) =
|
||||
|
||||
numEvents: state.runtime.numEvents,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
offset: state.runtime.offset,
|
||||
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset,
|
||||
|
||||
currentBlock: state.currentBlock,
|
||||
}));
|
||||
@@ -162,8 +172,11 @@ export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
||||
|
||||
export const useTimeUntilData = createSelector((state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
offset: state.runtime.offset,
|
||||
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset,
|
||||
offsetMode: state.runtime.offsetMode,
|
||||
currentDay: state.eventNow?.dayOffset ?? 0, //The day of the currently running event
|
||||
actualStart: state.runtime.actualStart,
|
||||
plannedStart: state.runtime.plannedStart,
|
||||
}));
|
||||
|
||||
export const useRuntimeOffset = createSelector((state: RuntimeStore) => ({
|
||||
@@ -179,6 +192,12 @@ export const useIsOnline = createSelector((state: RuntimeStore) => ({
|
||||
isOnline: state.ping > 0,
|
||||
}));
|
||||
|
||||
export const useOffsetMode = createSelector((state: RuntimeStore) => ({
|
||||
offsetMode: state.runtime.offsetMode,
|
||||
}));
|
||||
|
||||
export const setOffsetMode = (payload: OffsetMode) => socketSendJson('offsetmode', payload);
|
||||
|
||||
export const usePlayback = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type Size = 'xs' | 'sm' | 'md' | 'lg';
|
||||
@@ -11,7 +11,7 @@ type LogStore = {
|
||||
logs: Log[];
|
||||
};
|
||||
|
||||
const logger = createStore<LogStore>(() => ({
|
||||
export const logger = createStore<LogStore>(() => ({
|
||||
logs: [],
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { OntimeEntry, ProjectRundowns, Rundown } from 'ontime-types';
|
||||
|
||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../csv';
|
||||
import { makeCSVFromArrayOfArrays } from '../csv';
|
||||
|
||||
describe('makeCSVFromArrayOfArrays()', () => {
|
||||
it('joins an array of arrays with commas and newlines', () => {
|
||||
@@ -13,32 +11,3 @@ after newline,after comma
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateRundowns()', () => {
|
||||
it('flattens an object of rundowns into a single array', () => {
|
||||
const rundowns = {
|
||||
first: {
|
||||
id: '',
|
||||
title: '',
|
||||
revision: 0,
|
||||
order: ['1', '2'],
|
||||
entries: {
|
||||
'1': { id: '1' } as OntimeEntry,
|
||||
'2': { id: '2' } as OntimeEntry,
|
||||
},
|
||||
},
|
||||
second: {
|
||||
id: '',
|
||||
title: '',
|
||||
revision: 0,
|
||||
order: ['3', '4'],
|
||||
entries: {
|
||||
'3': { id: '3' } as OntimeEntry,
|
||||
'4': { id: '4' } as OntimeEntry,
|
||||
},
|
||||
} as Rundown,
|
||||
} as ProjectRundowns;
|
||||
|
||||
expect(aggregateRundowns(rundowns)).toStrictEqual([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEvent, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { EndAction, EventCustomFields, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||
|
||||
import { cloneEvent } from '../eventsManager';
|
||||
|
||||
describe('cloneEvent()', () => {
|
||||
it('creates a stem from a given event', () => {
|
||||
const original: OntimeEvent = {
|
||||
const original = {
|
||||
id: 'unique',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'title',
|
||||
@@ -14,9 +14,6 @@ describe('cloneEvent()', () => {
|
||||
duration: 10,
|
||||
timeEnd: 10,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: null,
|
||||
countToEnd: false,
|
||||
endAction: EndAction.None,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
@@ -24,39 +21,29 @@ describe('cloneEvent()', () => {
|
||||
revision: 10,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
custom: {
|
||||
lighting: '3',
|
||||
} as EntryCustomFields,
|
||||
};
|
||||
} as EventCustomFields,
|
||||
} as OntimeEvent;
|
||||
|
||||
const cloned = cloneEvent(original);
|
||||
expect(cloned).not.toBe(original);
|
||||
expect(cloned.custom).not.toBe(original.custom);
|
||||
|
||||
expect(cloned).toMatchObject({
|
||||
type: SupportedEvent.Event,
|
||||
title: original.title,
|
||||
note: original.note,
|
||||
timeStart: original.timeStart,
|
||||
duration: original.duration,
|
||||
timeEnd: original.timeEnd,
|
||||
timerType: original.timerType,
|
||||
timeStrategy: original.timeStrategy,
|
||||
countToEnd: original.countToEnd,
|
||||
linkStart: original.linkStart,
|
||||
endAction: original.endAction,
|
||||
isPublic: original.isPublic,
|
||||
skip: original.skip,
|
||||
colour: original.colour,
|
||||
revision: 0,
|
||||
delay: original.delay,
|
||||
dayOffset: original.dayOffset,
|
||||
gap: 0,
|
||||
timeWarning: original.timeWarning,
|
||||
timeDanger: original.timeDanger,
|
||||
});
|
||||
// @ts-expect-error -- safeguarding this
|
||||
expect(cloned?.id).toBe(undefined);
|
||||
expect(cloned.title).toBe(original.title);
|
||||
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);
|
||||
expect(cloned.timeWarning).toBe(original.timeWarning);
|
||||
expect(cloned.timeDanger).toBe(original.timeDanger);
|
||||
expect(cloned.custom).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { OffsetMode } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
import { calculateTimeUntilStart, formatTime, nowInMillis } from '../time';
|
||||
|
||||
describe('nowInMillis()', () => {
|
||||
@@ -40,82 +43,265 @@ describe('formatTime()', () => {
|
||||
});
|
||||
|
||||
describe('calculateTimeUntilStart()', () => {
|
||||
test('ontime', () => {
|
||||
describe('Absolute offset mode', () => {
|
||||
test('ontime', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 90,
|
||||
offset: 0,
|
||||
offsetMode: OffsetMode.Absolute,
|
||||
actualStart: null,
|
||||
plannedStart: null,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||
});
|
||||
|
||||
test('running behind', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 90,
|
||||
offset: -20,
|
||||
offsetMode: OffsetMode.Absolute,
|
||||
actualStart: null,
|
||||
plannedStart: null,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
|
||||
});
|
||||
|
||||
test('running ahead', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 80,
|
||||
offset: 10,
|
||||
offsetMode: OffsetMode.Absolute,
|
||||
actualStart: null,
|
||||
plannedStart: null,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||
});
|
||||
|
||||
test('running behind with enough gaps', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 20,
|
||||
clock: 50,
|
||||
offset: -20,
|
||||
offsetMode: OffsetMode.Absolute,
|
||||
actualStart: null,
|
||||
plannedStart: null,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50); // <-- when gap is enough to compensate for the running behind
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||
});
|
||||
|
||||
test('running behind with too little gaps', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 10,
|
||||
clock: 50,
|
||||
offset: -20,
|
||||
offsetMode: OffsetMode.Absolute,
|
||||
actualStart: 0,
|
||||
plannedStart: 0,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||
});
|
||||
});
|
||||
|
||||
describe('Relative offset mode', () => {
|
||||
test('basic function', () => {
|
||||
const test = {
|
||||
timeStart: 0,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 100,
|
||||
actualStart: 100,
|
||||
plannedStart: 0,
|
||||
offset: 0, // relative
|
||||
// offset: -100, // absolute
|
||||
offsetMode: OffsetMode.Relative,
|
||||
};
|
||||
|
||||
const timeStartEvent2 = 10;
|
||||
const timeStartEvent3 = 20;
|
||||
|
||||
//event 1 is the currently running event
|
||||
|
||||
//event 2
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(10);
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(10);
|
||||
|
||||
//event 3
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(20);
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(20);
|
||||
|
||||
// When clock advances by 5ms, time until start should decrease by 5ms
|
||||
test.clock = 105;
|
||||
|
||||
//event 2
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(5);
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(5);
|
||||
|
||||
//event 3
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(15);
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(15);
|
||||
});
|
||||
|
||||
test('gaps', () => {
|
||||
const test = {
|
||||
timeStart: 20,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 10,
|
||||
clock: 100,
|
||||
actualStart: 100,
|
||||
plannedStart: 0,
|
||||
offset: 0, // relative
|
||||
// offset: -100, // absolute
|
||||
offsetMode: OffsetMode.Relative,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
||||
|
||||
// When clock advances by 5ms, time until start should decrease by 5ms
|
||||
test.clock = 105;
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(15);
|
||||
});
|
||||
|
||||
test('added/remove time', () => {
|
||||
const test = {
|
||||
timeStart: 20,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 100,
|
||||
actualStart: 100,
|
||||
plannedStart: 0,
|
||||
offset: 0, // relative
|
||||
// offset: -100, // absolute
|
||||
offsetMode: OffsetMode.Relative,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
||||
|
||||
test.offset = 5; // remove 5 with addtime - we are ahead of time
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // unlocked evets will stay on schedule
|
||||
|
||||
test.offset = -5; // add 5 with addtime - we are behind
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(25);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(25);
|
||||
});
|
||||
|
||||
test('next day', () => {
|
||||
const test = {
|
||||
// timeStart: 10,
|
||||
// dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
// totalGap: 0,
|
||||
clock: 100,
|
||||
actualStart: 100,
|
||||
plannedStart: 0,
|
||||
offset: 0, // relative
|
||||
// offset: -100, // absolute
|
||||
offsetMode: OffsetMode.Relative,
|
||||
};
|
||||
|
||||
// this event will start the current day
|
||||
expect(
|
||||
calculateTimeUntilStart({
|
||||
...test,
|
||||
timeStart: 10,
|
||||
dayOffset: 0,
|
||||
totalGap: 0,
|
||||
isLinkedToLoaded: false,
|
||||
}),
|
||||
).toBe(10);
|
||||
|
||||
// this event will start the next day
|
||||
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
|
||||
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
|
||||
expect(
|
||||
calculateTimeUntilStart({
|
||||
...test,
|
||||
timeStart: 0,
|
||||
dayOffset: 1,
|
||||
totalGap: dayInMs - 20,
|
||||
isLinkedToLoaded: false,
|
||||
}),
|
||||
).toBe(dayInMs);
|
||||
|
||||
// advancing 100ms
|
||||
test.clock = 200;
|
||||
|
||||
expect(
|
||||
calculateTimeUntilStart({
|
||||
...test,
|
||||
timeStart: 0,
|
||||
dayOffset: 1,
|
||||
totalGap: dayInMs - 20,
|
||||
isLinkedToLoaded: false,
|
||||
}),
|
||||
).toBe(dayInMs - 100);
|
||||
});
|
||||
});
|
||||
|
||||
test('overlap with negative total gap', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
// timeStart: 20,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 90,
|
||||
offset: 0,
|
||||
// totalGap: 10,
|
||||
clock: 100,
|
||||
actualStart: 100,
|
||||
plannedStart: 0,
|
||||
offset: 0, // relative
|
||||
// offset: -100, // absolute
|
||||
offsetMode: OffsetMode.Relative,
|
||||
isLinkedToLoaded: false,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||
// the overlap will be pushed out to the expected available time
|
||||
expect(calculateTimeUntilStart({ ...test, timeStart: 5, totalGap: -5 })).toBe(10);
|
||||
|
||||
test.clock = 105;
|
||||
});
|
||||
|
||||
test('running behind', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 90,
|
||||
offset: -20,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
|
||||
});
|
||||
|
||||
test('running ahead', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 0,
|
||||
clock: 80,
|
||||
offset: 10,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||
});
|
||||
|
||||
test('running behind with enough gaps', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 20,
|
||||
clock: 50,
|
||||
offset: -20,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||
});
|
||||
|
||||
test('running behind with to little gaps', () => {
|
||||
const test = {
|
||||
timeStart: 100,
|
||||
dayOffset: 0,
|
||||
delay: 0,
|
||||
currentDay: 0,
|
||||
totalGap: 10,
|
||||
clock: 50,
|
||||
offset: -20,
|
||||
};
|
||||
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60);
|
||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||
});
|
||||
|
||||
//TODO: more indepth testing,
|
||||
// including day offset handling
|
||||
// and more?
|
||||
});
|
||||
|
||||
@@ -64,13 +64,13 @@ describe('getRouteFromPreset()', () => {
|
||||
describe('handle url sharing edge cases', () => {
|
||||
it('finds the correct preset when the url contains extra arguments', () => {
|
||||
const location = resolvePath('/demopage?locked=true&token=123');
|
||||
expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy();
|
||||
});
|
||||
expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('appends the feature params to the alias', () => {
|
||||
const location = resolvePath('/demopage?locked=true&token=123');
|
||||
expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123');
|
||||
});
|
||||
expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123')
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,26 +83,25 @@ describe('generatePathFromPreset()', () => {
|
||||
});
|
||||
|
||||
test('appends the feature params to the alias', () => {
|
||||
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe(
|
||||
'timer?user=guest&alias=demopage&locked=true&token=123',
|
||||
);
|
||||
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe('timer?user=guest&alias=demopage&locked=true&token=123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('arePathsEquivalent()', () => {
|
||||
it('checks whether the paths match', () => {
|
||||
it("checks whether the paths match", () => {
|
||||
expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer', 'timer')).toBeTruthy();
|
||||
expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy();
|
||||
});
|
||||
})
|
||||
|
||||
it('checks whether the params match', () => {
|
||||
it("checks whether the params match", () => {
|
||||
expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy();
|
||||
});
|
||||
})
|
||||
|
||||
it('considers edge cases for the url sharing feature', () => {
|
||||
it("considers edge cases for the url sharing feature", () => {
|
||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||
import { OntimeEntry, ProjectRundowns } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Converts an array of arrays to a CSV file
|
||||
* @description Converts an array of arrays to a CSV file
|
||||
* @param {string[][]} arrayOfArrays
|
||||
* @return {string}
|
||||
*/
|
||||
export function makeCSVFromArrayOfArrays(arrayOfArrays: string[][]): string {
|
||||
return stringify(arrayOfArrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives an object of rundowns, and flattens them into a single, linear rundown
|
||||
* Used for CSV export
|
||||
*/
|
||||
export function aggregateRundowns(rundowns: ProjectRundowns): OntimeEntry[] {
|
||||
const rundownKeys = Object.keys(rundowns);
|
||||
if (rundownKeys.length === 0) return [];
|
||||
const flatRundown: OntimeEntry[] = [];
|
||||
|
||||
for (const key of rundownKeys) {
|
||||
const { order, entries } = rundowns[key];
|
||||
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
const entryId = order[i];
|
||||
const entry = entries[entryId];
|
||||
|
||||
flatRundown.push(entry);
|
||||
}
|
||||
}
|
||||
return flatRundown;
|
||||
}
|
||||
|
||||
@@ -23,13 +23,12 @@ export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
||||
isPublic: event.isPublic,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
currentBlock: event.currentBlock,
|
||||
revision: 0,
|
||||
delay: event.delay, // the events will be collocated, so having the same metadata is a good start
|
||||
dayOffset: event.dayOffset,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: event.timeWarning,
|
||||
timeDanger: event.timeDanger,
|
||||
custom: { ...event.custom },
|
||||
custom: {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Log, Rundown, RuntimeStore } from 'ontime-types';
|
||||
import { Log, RundownCached, RuntimeStore } from 'ontime-types';
|
||||
|
||||
import { isProduction, websocketUrl } from '../../externals';
|
||||
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants';
|
||||
@@ -16,10 +16,11 @@ import { addDialog } from '../stores/dialogStore';
|
||||
import { addLog } from '../stores/logger';
|
||||
import { addToBatchUpdates, flushBatchUpdates, patchRuntime, patchRuntimeProperty } from '../stores/runtime';
|
||||
|
||||
let websocket: WebSocket | null = null;
|
||||
export 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;
|
||||
|
||||
@@ -49,14 +50,15 @@ export const connectSocket = () => {
|
||||
console.warn('WebSocket disconnected');
|
||||
setOnlineStatus(false);
|
||||
|
||||
// we decide to allows reconnect
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.warn('WebSocket: attempting reconnect');
|
||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||
reconnectAttempts += 1;
|
||||
connectSocket();
|
||||
}
|
||||
}, reconnectInterval);
|
||||
if (shouldReconnect) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.warn('WebSocket: attempting reconnect');
|
||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||
reconnectAttempts += 1;
|
||||
connectSocket();
|
||||
}
|
||||
}, reconnectInterval);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
@@ -201,7 +203,7 @@ export const connectSocket = () => {
|
||||
invalidateAllCaches();
|
||||
} else if (target === 'RUNDOWN') {
|
||||
const { revision } = payload;
|
||||
const currentRevision = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN)?.revision ?? -1;
|
||||
const currentRevision = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN)?.revision ?? -1;
|
||||
if (revision > currentRevision) {
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
||||
@@ -222,6 +224,11 @@ export const connectSocket = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const disconnectSocket = () => {
|
||||
shouldReconnect = false;
|
||||
websocket?.close();
|
||||
};
|
||||
|
||||
export const socketSend = (message: any) => {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(message);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MaybeNumber, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
||||
import { MaybeNumber, OffsetMode, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
||||
import { dayInMs, formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||
|
||||
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
||||
@@ -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
|
||||
*/
|
||||
function getFormatFromSettings(): TimeFormat {
|
||||
export function getFormatFromSettings(): TimeFormat {
|
||||
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||
return settings?.timeFormat ?? '24';
|
||||
}
|
||||
@@ -140,8 +140,8 @@ export function useTimeUntilStart(
|
||||
isLinkedToLoaded: boolean;
|
||||
},
|
||||
): number {
|
||||
const { offset, clock, currentDay } = useTimeUntilData();
|
||||
return calculateTimeUntilStart({ ...data, currentDay, clock, offset });
|
||||
const { offset, clock, currentDay, offsetMode, actualStart, plannedStart } = useTimeUntilData();
|
||||
return calculateTimeUntilStart({ ...data, currentDay, clock, offset, offsetMode, actualStart, plannedStart });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,9 +160,24 @@ export function calculateTimeUntilStart(
|
||||
isLinkedToLoaded: boolean;
|
||||
clock: number;
|
||||
offset: number;
|
||||
offsetMode: OffsetMode;
|
||||
actualStart: MaybeNumber;
|
||||
plannedStart: MaybeNumber;
|
||||
},
|
||||
): number {
|
||||
const { timeStart, dayOffset, currentDay, totalGap, isLinkedToLoaded, clock, offset, delay } = data;
|
||||
const {
|
||||
timeStart,
|
||||
dayOffset,
|
||||
currentDay,
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
clock,
|
||||
offset,
|
||||
delay,
|
||||
offsetMode,
|
||||
actualStart,
|
||||
plannedStart,
|
||||
} = data;
|
||||
|
||||
//How many days from the currently running event to this one
|
||||
const relativeDayOffset = dayOffset - currentDay;
|
||||
@@ -172,20 +187,23 @@ export function calculateTimeUntilStart(
|
||||
//The normalised start time of this event relative to the currently running event
|
||||
const normalisedTimeStart = delayedStart + relativeDayOffset * dayInMs;
|
||||
|
||||
const offsetTimestart = normalisedTimeStart - offset;
|
||||
const offsetTimeUntil = offsetTimestart - clock;
|
||||
let relativeStartOffset = 0;
|
||||
|
||||
if (offsetMode === OffsetMode.Relative) {
|
||||
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
|
||||
}
|
||||
|
||||
const scheduledTimeUntil = normalisedTimeStart - clock + relativeStartOffset;
|
||||
|
||||
const offsetTimeUntil = scheduledTimeUntil - offset;
|
||||
|
||||
if (isLinkedToLoaded) {
|
||||
//if we are directly linked back to the loaded event we just follow the offset
|
||||
return offsetTimeUntil;
|
||||
}
|
||||
|
||||
const scheduledTimeUntil = normalisedTimeStart - clock;
|
||||
|
||||
const isAheadOfSchedule = offset >= 0;
|
||||
const gapsCanCompensadeForOffset = totalGap + offset >= 0;
|
||||
|
||||
if (isAheadOfSchedule || gapsCanCompensadeForOffset) {
|
||||
const gapsCanCompensateForOffset = totalGap + offset >= 0;
|
||||
if (gapsCanCompensateForOffset) {
|
||||
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
|
||||
return scheduledTimeUntil;
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
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>;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
||||
|
||||
import style from './PanelContent.module.scss';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
||||
import { Button, IconButton, Input, Radio, RadioGroup, Select } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import {
|
||||
Automation,
|
||||
AutomationDTO,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { IoAdd, IoPencil, IoTrash } from 'react-icons/io5';
|
||||
import { Button, IconButton } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoPencil } from '@react-icons/all-files/io5/IoPencil';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { AutomationDTO, NormalisedAutomation } from 'ontime-types';
|
||||
|
||||
import { deleteAutomation } from '../../../../common/api/automation';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { NormalisedAutomation, Trigger } from 'ontime-types';
|
||||
|
||||
import { deleteTrigger } from '../../../../common/api/automation';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { IoPencil, IoTrash, IoWarningOutline } from 'react-icons/io5';
|
||||
import { IconButton } from '@chakra-ui/react';
|
||||
import { IoPencil } from '@react-icons/all-files/io5/IoPencil';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { IoWarningOutline } from '@react-icons/all-files/io5/IoWarningOutline';
|
||||
import { NormalisedAutomation, TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IoTrashBin } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoTrashBin } from '@react-icons/all-files/io5/IoTrashBin';
|
||||
|
||||
import { deleteAllReport } from '../../../../common/api/report';
|
||||
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
||||
@@ -29,8 +29,8 @@ export default function ReportSettings() {
|
||||
};
|
||||
|
||||
const combinedReport = useMemo(() => {
|
||||
return getCombinedReport(reportData, data.entries, data.order);
|
||||
}, [reportData, data.entries, data.order]);
|
||||
return getCombinedReport(reportData, data.rundown, data.order);
|
||||
}, [reportData, data.rundown, data.order]);
|
||||
|
||||
return (
|
||||
<Panel.Section>
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { IoAdd, IoOpenOutline, IoTrash } from 'react-icons/io5';
|
||||
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
|
||||
import { postUrlPresets } from '../../../../common/api/urlPresets';
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { IoPencil, IoTrash } from 'react-icons/io5';
|
||||
import { IconButton } from '@chakra-ui/react';
|
||||
import { IoPencil } from '@react-icons/all-files/io5/IoPencil';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
||||
|
||||
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
||||
|
||||
import { deleteCustomField, editCustomField, postCustomField } from '../../../../../common/api/customFields';
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { EntryId, isOntimeEvent, MaybeNumber, OntimeReport, RundownEntries } from 'ontime-types';
|
||||
import { isOntimeEvent, MaybeNumber, NormalisedRundown, OntimeReport } from 'ontime-types';
|
||||
|
||||
import { makeCSVFromArrayOfArrays } from '../../../../common/utils/csv';
|
||||
import { formatTime } from '../../../../common/utils/time';
|
||||
@@ -16,7 +16,7 @@ export type CombinedReport = {
|
||||
/**
|
||||
* Creates a combined report with the rundown data
|
||||
*/
|
||||
export function getCombinedReport(report: OntimeReport, rundown: RundownEntries, order: EntryId[]): CombinedReport[] {
|
||||
export function getCombinedReport(report: OntimeReport, rundown: NormalisedRundown, order: string[]): CombinedReport[] {
|
||||
if (Object.keys(report).length === 0) return [];
|
||||
if (order.length === 0) return [];
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ 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,7 +1,7 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { IoEyeOutline } from 'react-icons/io5';
|
||||
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
||||
import { IoEyeOutline } from '@react-icons/all-files/io5/IoEyeOutline';
|
||||
import { Settings } from 'ontime-types';
|
||||
|
||||
import { isAlphanumeric } from '../../../../common/utils/regex';
|
||||
|
||||
@@ -102,6 +102,7 @@ export default function EditorSettingsForm() {
|
||||
onChange={(event) => setDefaultEndAction(event.target.value as EndAction)}
|
||||
>
|
||||
<option value={EndAction.None}>None</option>
|
||||
<option value={EndAction.Stop}>Stop</option>
|
||||
<option value={EndAction.LoadNext}>Load next</option>
|
||||
<option value={EndAction.PlayNext}>Play next</option>
|
||||
</Select>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IoArrowUp } from 'react-icons/io5';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
|
||||
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
|
||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { IoArrowUp } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
|
||||
import { handleLinks } from '../../../../common/utils/linkUtils';
|
||||
import Log from '../../../log/Log';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Button, Input } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
|
||||
import { uploadProjectFile } from '../../../../common/api/db';
|
||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChangeEvent, useEffect, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { IoDownloadOutline, IoTrash } from 'react-icons/io5';
|
||||
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { type ProjectData } from 'ontime-types';
|
||||
|
||||
import { projectLogoPath } from '../../../../common/api/constants';
|
||||
@@ -96,7 +97,7 @@ export default function ProjectData() {
|
||||
const isLoading = status === 'pending';
|
||||
|
||||
return (
|
||||
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event, onReset)}>
|
||||
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event, onReset)}>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>
|
||||
Project data
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||
|
||||
import {
|
||||
deleteProject,
|
||||
|
||||
@@ -6,7 +6,7 @@ export async function makeProjectPatch(data: DatabaseModel, mergeKeys: Record<st
|
||||
for (const key in mergeKeys) {
|
||||
if (isKeyOfType(key, data) && mergeKeys[key]) {
|
||||
// if the rundown is merged we also need the custom fields
|
||||
if (key === 'rundowns') {
|
||||
if (key === 'rundown') {
|
||||
patchObject.customFields = data['customFields'];
|
||||
}
|
||||
Object.assign(patchObject, { [key]: data[key] });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { IoCheckmark, IoShieldCheckmarkOutline } from 'react-icons/io5';
|
||||
import { Button, Input, Spinner } from '@chakra-ui/react';
|
||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||
import { IoShieldCheckmarkOutline } from '@react-icons/all-files/io5/IoShieldCheckmarkOutline';
|
||||
|
||||
import { getWorksheetNames } from '../../../../common/api/sheets';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { CustomFields, Rundown } from 'ontime-types';
|
||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
@@ -9,7 +9,7 @@ import useGoogleSheet from './useGoogleSheet';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
interface ImportReviewProps {
|
||||
rundown: Rundown;
|
||||
rundown: OntimeRundown;
|
||||
customFields: CustomFields;
|
||||
onFinished: () => void;
|
||||
onCancel: () => void;
|
||||
@@ -29,12 +29,7 @@ export default function ImportReview(props: ImportReviewProps) {
|
||||
|
||||
const applyImport = async () => {
|
||||
setLoading(true);
|
||||
await importRundown(
|
||||
{
|
||||
[rundown.id]: rundown,
|
||||
},
|
||||
customFields,
|
||||
);
|
||||
await importRundown(rundown, customFields);
|
||||
setLoading(false);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import { IoCloudOutline, IoDownloadOutline } from 'react-icons/io5';
|
||||
import { Button, Input } from '@chakra-ui/react';
|
||||
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
||||
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
|
||||
import { getErrorMessage, ImportMap } from 'ontime-utils';
|
||||
|
||||
import {
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
||||
import { Button, IconButton, Input, Select, Tooltip } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
||||
|
||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { ImportCustom, ImportMap } from 'ontime-utils';
|
||||
export type NamedImportMap = typeof namedImportMap;
|
||||
|
||||
// Record of label and import name
|
||||
const namedImportMap = {
|
||||
export const namedImportMap = {
|
||||
Worksheet: 'event schedule',
|
||||
Start: 'time start',
|
||||
'Link start': 'link start',
|
||||
|
||||
+32
-33
@@ -1,6 +1,6 @@
|
||||
import { Fragment } from 'react';
|
||||
import { IoLink } from 'react-icons/io5';
|
||||
import { CustomFields, isOntimeBlock, isOntimeEvent, Rundown } from 'ontime-types';
|
||||
import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
||||
import { CustomFields, isOntimeBlock, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import Tag from '../../../../../common/components/tag/Tag';
|
||||
@@ -10,7 +10,7 @@ import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
import style from './PreviewRundown.module.scss';
|
||||
|
||||
interface PreviewRundownProps {
|
||||
rundown: Rundown;
|
||||
rundown: OntimeRundown;
|
||||
customFields: CustomFields;
|
||||
}
|
||||
|
||||
@@ -53,76 +53,75 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rundown.order.map((entryId) => {
|
||||
const entry = rundown.entries[entryId];
|
||||
if (isOntimeBlock(entry)) {
|
||||
{rundown.map((event) => {
|
||||
if (isOntimeBlock(event)) {
|
||||
return (
|
||||
<tr key={entry.id}>
|
||||
<tr key={event.id}>
|
||||
<td className={style.center}>
|
||||
<Tag>-</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{entry.type}</Tag>
|
||||
<Tag>{event.type}</Tag>
|
||||
</td>
|
||||
<td />
|
||||
<td colSpan={99}>{entry.title}</td>
|
||||
<td colSpan={99}>{event.title}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
if (!isOntimeEvent(entry)) {
|
||||
if (!isOntimeEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
eventIndex += 1;
|
||||
const colour = entry.colour ? getAccessibleColour(entry.colour) : {};
|
||||
const countToEnd = booleanToText(entry.countToEnd);
|
||||
const isPublic = booleanToText(entry.isPublic);
|
||||
const skip = booleanToText(entry.skip);
|
||||
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
||||
const countToEnd = booleanToText(event.countToEnd);
|
||||
const isPublic = booleanToText(event.isPublic);
|
||||
const skip = booleanToText(event.skip);
|
||||
|
||||
return (
|
||||
<Fragment key={entry.id}>
|
||||
<Fragment key={event.id}>
|
||||
<tr>
|
||||
<td className={style.center}>
|
||||
<Tag>{eventIndex}</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{entry.type}</Tag>
|
||||
<Tag>{event.type}</Tag>
|
||||
</td>
|
||||
<td className={style.nowrap}>{entry.cue}</td>
|
||||
<td>{entry.title}</td>
|
||||
<td className={style.nowrap}>{event.cue}</td>
|
||||
<td>{event.title}</td>
|
||||
<td className={style.flex}>
|
||||
<span className={entry.linkStart ? style.subdued : undefined}>{millisToString(entry.timeStart)}</span>
|
||||
{entry.linkStart && <IoLink className={style.linkStartActive} />}
|
||||
<span className={event.linkStart ? style.subdued : undefined}>{millisToString(event.timeStart)}</span>
|
||||
{event.linkStart && <IoLink className={style.linkStartActive} />}
|
||||
</td>
|
||||
<td>{millisToString(entry.timeEnd)}</td>
|
||||
<td>{millisToString(entry.duration)}</td>
|
||||
<td>{millisToString(entry.timeWarning)}</td>
|
||||
<td>{millisToString(entry.timeDanger)}</td>
|
||||
<td>{millisToString(event.timeEnd)}</td>
|
||||
<td>{millisToString(event.duration)}</td>
|
||||
<td>{millisToString(event.timeWarning)}</td>
|
||||
<td>{millisToString(event.timeDanger)}</td>
|
||||
<td className={style.center}>{countToEnd && <Tag>{countToEnd}</Tag>}</td>
|
||||
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
||||
<td>{skip && <Tag>{skip}</Tag>}</td>
|
||||
<td style={{ ...colour }}>{entry.colour}</td>
|
||||
<td style={{ ...colour }}>{event.colour}</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{entry.timerType}</Tag>
|
||||
<Tag>{event.timerType}</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{entry.endAction}</Tag>
|
||||
<Tag>{event.endAction}</Tag>
|
||||
</td>
|
||||
{isOntimeEvent(entry) &&
|
||||
{isOntimeEvent(event) &&
|
||||
fieldKeys.map((field) => {
|
||||
let value = '';
|
||||
if (field in entry.custom) {
|
||||
value = entry.custom[field];
|
||||
if (field in event.custom) {
|
||||
value = event.custom[field];
|
||||
}
|
||||
return <td key={field}>{value}</td>;
|
||||
})}
|
||||
<td className={style.center}>
|
||||
<Tag>{entry.id}</Tag>
|
||||
<Tag>{event.id}</Tag>
|
||||
</td>
|
||||
</tr>
|
||||
{entry.note && (
|
||||
{event.note && (
|
||||
<tr>
|
||||
<td colSpan={99} className={style.secondaryRow}>
|
||||
Note: {entry.note}
|
||||
Note: {event.note}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AuthenticationStatus, CustomFields, ProjectRundowns } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
|
||||
@@ -75,9 +75,9 @@ export default function useGoogleSheet() {
|
||||
};
|
||||
|
||||
/** applies rundown and customFields to current project */
|
||||
const importRundown = async (rundowns: ProjectRundowns, customFields: CustomFields) => {
|
||||
const importRundown = async (rundown: OntimeRundown, customFields: CustomFields) => {
|
||||
try {
|
||||
await patchData({ rundowns, customFields });
|
||||
await patchData({ rundown, customFields });
|
||||
// we are unable to optimistically set the rundown since we need
|
||||
// it to be normalised
|
||||
await queryClient.invalidateQueries({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { defaultImportMap, ImportMap } from 'ontime-utils';
|
||||
import { create } from 'zustand';
|
||||
|
||||
@@ -15,8 +15,8 @@ type SheetStore = {
|
||||
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
||||
|
||||
// we get this from a preview response
|
||||
rundown: Rundown | null;
|
||||
setRundown: (rundown: Rundown | null) => void;
|
||||
rundown: OntimeRundown | null;
|
||||
setRundown: (rundown: OntimeRundown | null) => void;
|
||||
|
||||
// we get this from a preview response
|
||||
customFields: CustomFields | null;
|
||||
@@ -60,7 +60,7 @@ export const useSheetStore = create<SheetStore>((set, get) => ({
|
||||
|
||||
setAuthenticationStatus: (status: AuthenticationStatus) => set({ authenticationStatus: status }),
|
||||
|
||||
setRundown: (rundown: Rundown | null) => set({ rundown }),
|
||||
setRundown: (rundown: OntimeRundown | null) => set({ rundown }),
|
||||
|
||||
setCustomFields: (customFields: CustomFields | null) => set({ customFields }),
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { IoEye, IoEyeOffOutline } from 'react-icons/io5';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
||||
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
||||
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { IoArrowDown, IoArrowUp, IoBan, IoFlag, IoTime } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
|
||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||
import { TimerPhase, TimerType } from 'ontime-types';
|
||||
|
||||
import { useMessagePreview } from '../../../common/hooks/useSocket';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IoAdd, IoRemove } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { Playback } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_SECOND, parseUserTime } from 'ontime-utils';
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { IoArrowDown, IoArrowUp, IoPause, IoPlay, IoStop } from 'react-icons/io5';
|
||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { Playback, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||
import { parseUserTime } from 'ontime-utils';
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IoPause, IoPlay, IoPlaySkipBack, IoPlaySkipForward, IoReload, IoStop, IoTime } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||
import { Playback, TimerPhase } from 'ontime-types';
|
||||
import { validatePlayback } from 'ontime-utils';
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { lazy, useCallback, useEffect } from 'react';
|
||||
import { IoApps, IoClose, IoSettingsOutline } from 'react-icons/io5';
|
||||
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
|
||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useElectronListener } from '../../common/hooks/useElectronEvent';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HTMLAttributes, LabelHTMLAttributes } from 'react';
|
||||
import { IconBaseProps } from 'react-icons';
|
||||
import { IoArrowUp } from 'react-icons/io5';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { type IconBaseProps } from '@react-icons/all-files/lib';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
|
||||
@@ -126,8 +126,8 @@ export default function Operator() {
|
||||
let isPast = Boolean(featureData.selectedEventId);
|
||||
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||
|
||||
const { firstEvent } = getFirstEventNormal(data.entries, data.order);
|
||||
const { lastEvent } = getLastEventNormal(data.entries, data.order);
|
||||
const { firstEvent } = getFirstEventNormal(data.rundown, data.order);
|
||||
const { lastEvent } = getLastEventNormal(data.rundown, data.order);
|
||||
|
||||
return (
|
||||
<div className={style.operatorContainer}>
|
||||
@@ -152,7 +152,7 @@ export default function Operator() {
|
||||
|
||||
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
||||
{data.order.map((eventId) => {
|
||||
const entry = data.entries[eventId];
|
||||
const entry = data.rundown[eventId];
|
||||
if (isOntimeEvent(entry)) {
|
||||
const isSelected = featureData.selectedEventId === entry.id;
|
||||
if (isSelected) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IoLocate } from 'react-icons/io5';
|
||||
import { IoLocate } from '@react-icons/all-files/io5/IoLocate';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ function RuntimeOverview() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimeColumn label='Offset' value={offsetText} className={offsetClasses} />
|
||||
<TimeColumn label='Offset' value={offsetText} className={offsetClasses} testId='offset' />
|
||||
<TimeColumn label='Time now' value={formatedTime(clock)} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -10,13 +10,16 @@ interface TimeLayoutProps {
|
||||
muted?: boolean;
|
||||
daySpan?: number;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function TimeColumn({ label, value, muted, className }: TimeLayoutProps) {
|
||||
export function TimeColumn({ label, value, muted, className, testId }: TimeLayoutProps) {
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<span className={style.label}>{label}</span>
|
||||
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
|
||||
<span className={cx([style.clock, muted && style.muted, className])} data-testid={testId}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-right: 4px; // size of scrollbar
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -15,7 +16,6 @@
|
||||
overflow-x: clip;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import {
|
||||
type EntryId,
|
||||
type MaybeString,
|
||||
type PlayableEvent,
|
||||
type Rundown,
|
||||
isOntimeBlock,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
MaybeString,
|
||||
PlayableEvent,
|
||||
Playback,
|
||||
RundownCached,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
getPreviousBlockNormal,
|
||||
getPreviousNormal,
|
||||
isNewLatest,
|
||||
reorderArray,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { type EventOptions, useEventAction } from '../../common/hooks/useEventAction';
|
||||
@@ -41,12 +39,12 @@ import style from './Rundown.module.scss';
|
||||
const RundownEntry = lazy(() => import('./RundownEntry'));
|
||||
|
||||
interface RundownProps {
|
||||
data: Rundown;
|
||||
data: RundownCached;
|
||||
}
|
||||
|
||||
export default function Rundown({ data }: RundownProps) {
|
||||
const { order, entries } = data;
|
||||
const [statefulEntries, setStatefulEntries] = useState<EntryId[]>(order);
|
||||
const { order, rundown } = data;
|
||||
const [statefulEntries, setStatefulEntries] = useState(order);
|
||||
|
||||
const featureData = useRundownEditor();
|
||||
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
||||
@@ -67,30 +65,30 @@ export default function Rundown({ data }: RundownProps) {
|
||||
const deleteAtCursor = useCallback(
|
||||
(cursor: string | null) => {
|
||||
if (!cursor) return;
|
||||
const { entry, index } = getPreviousNormal(entries, order, cursor);
|
||||
const { entry, index } = getPreviousNormal(rundown, order, cursor);
|
||||
deleteEvent([cursor]);
|
||||
if (entry && index !== null) {
|
||||
setSelectedEvents({ id: entry.id, selectMode: 'click', index });
|
||||
}
|
||||
},
|
||||
[entries, order, deleteEvent, setSelectedEvents],
|
||||
[rundown, order, deleteEvent, setSelectedEvents],
|
||||
);
|
||||
|
||||
const insertCopyAtId = useCallback(
|
||||
(atId: string | null, copyId: string | null, above = false) => {
|
||||
const adjustedCursor = above ? getPreviousNormal(entries, order, atId ?? '').entry?.id ?? null : atId;
|
||||
const adjustedCursor = above ? getPreviousNormal(rundown, order, atId ?? '').entry?.id ?? null : atId;
|
||||
if (copyId === null) {
|
||||
// we cant clone without selection
|
||||
return;
|
||||
}
|
||||
const cloneEntry = entries[copyId];
|
||||
const cloneEntry = rundown[copyId];
|
||||
if (cloneEntry?.type === SupportedEvent.Event) {
|
||||
//if we don't have a cursor add the new event on top
|
||||
const newEvent = cloneEvent(cloneEntry);
|
||||
addEvent(newEvent, { after: adjustedCursor ?? undefined });
|
||||
}
|
||||
},
|
||||
[addEvent, order, entries],
|
||||
[addEvent, order, rundown],
|
||||
);
|
||||
|
||||
const insertAtId = useCallback(
|
||||
@@ -126,7 +124,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
let newCursor = cursor;
|
||||
if (cursor === null) {
|
||||
// there is no cursor, we select the first or last depending on direction
|
||||
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||
const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
|
||||
|
||||
if (isOntimeBlock(selected)) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
@@ -142,14 +140,14 @@ export default function Rundown({ data }: RundownProps) {
|
||||
// otherwise we select the next or previous
|
||||
const selected =
|
||||
direction === 'up'
|
||||
? getPreviousBlockNormal(entries, order, newCursor)
|
||||
: getNextBlockNormal(entries, order, newCursor);
|
||||
? getPreviousBlockNormal(rundown, order, newCursor)
|
||||
: getNextBlockNormal(rundown, order, newCursor);
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
}
|
||||
},
|
||||
[order, entries, setSelectedEvents],
|
||||
[order, rundown, setSelectedEvents],
|
||||
);
|
||||
|
||||
const selectEntry = useCallback(
|
||||
@@ -160,7 +158,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
|
||||
if (cursor === null) {
|
||||
// there is no cursor, we select the first or last depending on direction if it exists
|
||||
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||
const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
|
||||
if (selected !== null) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
}
|
||||
@@ -169,13 +167,13 @@ export default function Rundown({ data }: RundownProps) {
|
||||
|
||||
// otherwise we select the next or previous
|
||||
const selected =
|
||||
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
|
||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
}
|
||||
},
|
||||
[order, entries, setSelectedEvents],
|
||||
[order, rundown, setSelectedEvents],
|
||||
);
|
||||
|
||||
const moveEntry = useCallback(
|
||||
@@ -184,14 +182,14 @@ export default function Rundown({ data }: RundownProps) {
|
||||
return;
|
||||
}
|
||||
const { index } =
|
||||
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
|
||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
||||
|
||||
if (index !== null) {
|
||||
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
||||
reorderEvent(cursor, offsetIndex, index);
|
||||
}
|
||||
},
|
||||
[order, reorderEvent, entries],
|
||||
[order, reorderEvent, rundown],
|
||||
);
|
||||
|
||||
// shortcuts
|
||||
@@ -240,9 +238,6 @@ export default function Rundown({ data }: RundownProps) {
|
||||
setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index });
|
||||
}, [appMode, featureData.selectedEventId, order, setSelectedEvents]);
|
||||
|
||||
/**
|
||||
* On drag end, we reorder the events
|
||||
*/
|
||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
@@ -250,10 +245,9 @@ export default function Rundown({ data }: RundownProps) {
|
||||
if (active.id !== over?.id) {
|
||||
const fromIndex = active.data.current?.sortable.index;
|
||||
const toIndex = over.data.current?.sortable.index;
|
||||
|
||||
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
||||
// ugly hack to handle inconsistencies between dnd-kit and async store updates
|
||||
setStatefulEntries((currentEntries) => {
|
||||
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||
return arrayMove(currentEntries, fromIndex, toIndex);
|
||||
});
|
||||
reorderEvent(String(active.id), fromIndex, toIndex);
|
||||
}
|
||||
@@ -265,11 +259,11 @@ export default function Rundown({ data }: RundownProps) {
|
||||
}
|
||||
|
||||
// last event is used to calculate relative timings
|
||||
let lastEvent: PlayableEvent | null = null; // used by indicators
|
||||
let thisEvent: PlayableEvent | null = null;
|
||||
let lastEvent: PlayableEvent | undefined; // used by indicators
|
||||
let thisEvent: PlayableEvent | undefined;
|
||||
// previous entry is used to infer position in the rundown for new events
|
||||
let previousEntryId: MaybeString = null;
|
||||
let thisId: MaybeString = null;
|
||||
let previousEntryId: string | undefined;
|
||||
let thisId = previousEntryId;
|
||||
|
||||
let eventIndex = 0;
|
||||
// all events before the current selected are in the past
|
||||
@@ -278,7 +272,6 @@ export default function Rundown({ data }: RundownProps) {
|
||||
let totalGap = 0;
|
||||
const isEditMode = appMode === AppMode.Edit;
|
||||
let isLinkedToLoaded = true; //check if the event can link all the way back to the currently playing event
|
||||
|
||||
return (
|
||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
||||
@@ -288,7 +281,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
// we iterate through a stateful copy of order to make the operations smoother
|
||||
// this means that this can be out of sync with order until the useEffect runs
|
||||
// instead of writing all the logic guards, we simply short circuit rendering here
|
||||
const entry = entries[entryId];
|
||||
const entry = rundown[entryId];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
MaybeString,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||
@@ -37,13 +28,13 @@ export type EventItemActions =
|
||||
interface RundownEntryProps {
|
||||
type: SupportedEvent;
|
||||
isPast: boolean;
|
||||
data: OntimeEntry;
|
||||
data: OntimeRundownEntry;
|
||||
loaded: boolean;
|
||||
eventIndex: number;
|
||||
hasCursor: boolean;
|
||||
isNext: boolean;
|
||||
isNextDay: boolean;
|
||||
previousEntryId: MaybeString;
|
||||
previousEntryId?: string;
|
||||
previousEventId?: string;
|
||||
playback?: Playback; // we only care about this if this event is playing
|
||||
isRolling: boolean; // we need to know even if not related to this event
|
||||
@@ -159,7 +150,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
}
|
||||
});
|
||||
|
||||
if (isOntimeEvent(data)) {
|
||||
if (data.type === SupportedEvent.Event) {
|
||||
return (
|
||||
<EventBlock
|
||||
eventId={data.id}
|
||||
@@ -176,7 +167,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
timerType={data.timerType}
|
||||
title={data.title}
|
||||
note={data.note}
|
||||
delay={data.delay}
|
||||
delay={data.delay ?? 0}
|
||||
colour={data.colour}
|
||||
isPast={isPast}
|
||||
isNext={isNext}
|
||||
@@ -193,15 +184,9 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
);
|
||||
} else if (isOntimeBlock(data)) {
|
||||
return (
|
||||
<BlockBlock data={data} hasCursor={hasCursor}>
|
||||
{data.events.map((eventId) => {
|
||||
return <div key={eventId}>{eventId}</div>;
|
||||
})}
|
||||
</BlockBlock>
|
||||
);
|
||||
} else if (isOntimeDelay(data)) {
|
||||
} else if (data.type === SupportedEvent.Block) {
|
||||
return <BlockBlock data={data} hasCursor={hasCursor} onDelete={() => actionHandler('delete')} />;
|
||||
} else if (data.type === SupportedEvent.Delay) {
|
||||
return <DelayBlock data={data} hasCursor={hasCursor} />;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
.list {
|
||||
@include editor.panel;
|
||||
height: inherit;
|
||||
padding-inline: 0;
|
||||
padding-left: 0;
|
||||
box-shadow: $box-shadow-right;
|
||||
|
||||
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
|
||||
|
||||
@@ -21,12 +21,18 @@ $block-cursor-color: $orange-400;
|
||||
border: 1px solid $white-7;
|
||||
border-radius: $block-border-radius;
|
||||
margin-block: 0.25rem;
|
||||
margin-right: 0.125rem;
|
||||
position: relative;
|
||||
color: $block-text-color;
|
||||
|
||||
min-width: $block-width;
|
||||
}
|
||||
|
||||
@mixin block-spacing() {
|
||||
padding-right: 0.5rem;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
@mixin drag-style() {
|
||||
font-size: 20px;
|
||||
justify-self: center;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
.block {
|
||||
@include block-spacing;
|
||||
@include block-styling;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
background-color: $block-bg2;
|
||||
|
||||
@@ -20,3 +22,7 @@
|
||||
.drag {
|
||||
@include drag-style;
|
||||
}
|
||||
|
||||
.actionMenu {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { PropsWithChildren, useRef } from 'react';
|
||||
import { IoReorderTwo } from 'react-icons/io5';
|
||||
import { useRef } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import { OntimeBlock } from 'ontime-types';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||
|
||||
import BlockDelete from './BlockDelete';
|
||||
|
||||
import style from './BlockBlock.module.scss';
|
||||
|
||||
interface BlockBlockProps {
|
||||
data: OntimeBlock;
|
||||
hasCursor: boolean;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export default function BlockBlock(props: PropsWithChildren<BlockBlockProps>) {
|
||||
const { data, hasCursor, children } = props;
|
||||
export default function BlockBlock(props: BlockBlockProps) {
|
||||
const { data, hasCursor, onDelete } = props;
|
||||
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
@@ -43,8 +46,7 @@ export default function BlockBlock(props: PropsWithChildren<BlockBlockProps>) {
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
||||
<button>+++</button>
|
||||
<div>{children}</div>
|
||||
<BlockDelete onDelete={onDelete} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IconButton } from '@chakra-ui/react';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
|
||||
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';
|
||||
|
||||
interface BlockDeleteProps {
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export default function BlockDelete(props: BlockDeleteProps) {
|
||||
const { onDelete } = props;
|
||||
const mode = useAppMode((state) => state.mode);
|
||||
|
||||
const isRunMode = mode === AppMode.Run;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label='Delete'
|
||||
size='sm'
|
||||
icon={<IoTrash />}
|
||||
variant='ontime-subtle'
|
||||
color='#FA5656'
|
||||
onClick={onDelete}
|
||||
isDisabled={isRunMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
.delay {
|
||||
@include block-spacing;
|
||||
@include block-styling;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
background-color: $block-bg2;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2rem 1fr auto;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { IoCheckmarkDone, IoClose, IoReorderTwo } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { IoCheckmarkDone } from '@react-icons/all-files/io5/IoCheckmarkDone';
|
||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import { OntimeDelay } from 'ontime-types';
|
||||
|
||||
import DelayInput from '../../../common/components/input/delay-input/DelayInput';
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
IoAdd,
|
||||
IoDuplicateOutline,
|
||||
IoLink,
|
||||
IoPeople,
|
||||
IoPeopleOutline,
|
||||
IoReorderTwo,
|
||||
IoSwapVertical,
|
||||
IoTrash,
|
||||
IoUnlink,
|
||||
} from 'react-icons/io5';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
|
||||
import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
||||
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
|
||||
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink';
|
||||
import { EndAction, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import {
|
||||
IoArrowDown,
|
||||
IoArrowUp,
|
||||
IoBan,
|
||||
IoFlag,
|
||||
IoPeople,
|
||||
IoPlay,
|
||||
IoPlayForward,
|
||||
IoPlaySkipForward,
|
||||
IoTime,
|
||||
} from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
|
||||
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { IoPlayForward } from '@react-icons/all-files/io5/IoPlayForward';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||
import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
@@ -176,6 +175,9 @@ function EndActionIcon(props: { action: EndAction; className: string }) {
|
||||
if (action === EndAction.PlayNext) {
|
||||
return <IoPlayForward className={maybeActiveClasses} />;
|
||||
}
|
||||
if (action === EndAction.Stop) {
|
||||
return <IoStop className={maybeActiveClasses} />;
|
||||
}
|
||||
return <IoPlay className={className} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IoCheckmarkCircle } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle';
|
||||
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||
|
||||
import { usePlayback } from '../../../../common/hooks/useSocket';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user