mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-04 23:18:01 +00:00
Compare commits
8 Commits
custom-fields
...
cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
| ffde3739d1 | |||
| 9db8d996e9 | |||
| 59c7360ea2 | |||
| f8bf867576 | |||
| dda165bfd7 | |||
| dbf8eeb7fb | |||
| 433e9f6a4f | |||
| 7958d49153 |
Generated
-5
@@ -1,5 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -10,7 +10,6 @@ export const PROJECT_DATA = ['project'];
|
|||||||
export const PROJECT_LIST = ['projectList'];
|
export const PROJECT_LIST = ['projectList'];
|
||||||
export const RUNDOWN = ['rundown'];
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
export const SHEET_STATE = ['sheetState'];
|
|
||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
export const CLIENT_LIST = ['clientList'];
|
export const CLIENT_LIST = ['clientList'];
|
||||||
@@ -19,11 +18,8 @@ export const REPORT = ['report'];
|
|||||||
// API URLs
|
// API URLs
|
||||||
export const apiEntryUrl = `${serverURL}/data`;
|
export const apiEntryUrl = `${serverURL}/data`;
|
||||||
|
|
||||||
export const projectDataURL = `${serverURL}/project`;
|
const userAssetsPath = 'user';
|
||||||
export const rundownURL = `${serverURL}/events`;
|
const cssOverridePath = 'styles/override.css';
|
||||||
export const ontimeURL = `${serverURL}/ontime`;
|
|
||||||
|
|
||||||
export const userAssetsPath = 'user';
|
|
||||||
export const cssOverridePath = 'styles/override.css';
|
|
||||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
||||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
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 { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||||
import { makeCSVFromArrayOfArrays } from '../utils/csv';
|
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import { createBlob, downloadBlob } from './utils';
|
import { createBlob, downloadBlob } from './utils';
|
||||||
@@ -40,9 +40,11 @@ export async function downloadProject(fileName: string) {
|
|||||||
export async function downloadCSV(fileName: string = 'rundown') {
|
export async function downloadCSV(fileName: string = 'rundown') {
|
||||||
try {
|
try {
|
||||||
const { data, name } = await fileDownload(fileName);
|
const { data, name } = await fileDownload(fileName);
|
||||||
const { project, rundown, customFields } = data;
|
const { project, rundowns, customFields } = data;
|
||||||
|
|
||||||
|
const flatRundowns = aggregateRundowns(rundowns);
|
||||||
|
const sheetData = makeTable(project, flatRundowns, customFields);
|
||||||
|
|
||||||
const sheetData = makeTable(project, rundown, customFields);
|
|
||||||
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
||||||
|
|
||||||
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
import { CustomFields, Rundown } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
const excelPath = `${apiEntryUrl}/excel`;
|
const excelPath = `${apiEntryUrl}/excel`;
|
||||||
|
|
||||||
type PreviewSpreadsheetResponse = {
|
|
||||||
rundown: OntimeRundown;
|
|
||||||
customFields: CustomFields;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* upload Excel file to server
|
* upload Excel file to server
|
||||||
* @return string - file ID op the uploaded file
|
* @return string - file ID op the uploaded file
|
||||||
@@ -34,6 +29,10 @@ export async function getWorksheetNames(): Promise<string[]> {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreviewSpreadsheetResponse = {
|
||||||
|
rundown: Rundown;
|
||||||
|
customFields: CustomFields;
|
||||||
|
};
|
||||||
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||||
options,
|
options,
|
||||||
|
|||||||
@@ -1,29 +1,44 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
import {
|
||||||
|
MessageResponse,
|
||||||
|
OntimeEntry,
|
||||||
|
OntimeEvent,
|
||||||
|
ProjectRundownsList,
|
||||||
|
Rundown,
|
||||||
|
TransientEventPayload,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
const rundownPath = `${apiEntryUrl}/rundown`;
|
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
|
* HTTP request to fetch all events
|
||||||
*/
|
*/
|
||||||
export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||||
const res = await axios.get(`${rundownPath}/normalised`);
|
const res = await axios.get(`${rundownPath}/current`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to post new event
|
* HTTP request to post new event
|
||||||
*/
|
*/
|
||||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
return axios.post(rundownPath, data);
|
return axios.post(rundownPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to put new event
|
* HTTP request to put new event
|
||||||
*/
|
*/
|
||||||
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function requestPutEvent(data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
return axios.put(rundownPath, data);
|
return axios.put(rundownPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +63,7 @@ export type ReorderEntry = {
|
|||||||
/**
|
/**
|
||||||
* HTTP request to reorder events
|
* HTTP request to reorder events
|
||||||
*/
|
*/
|
||||||
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
return axios.patch(`${rundownPath}/reorder`, data);
|
return axios.patch(`${rundownPath}/reorder`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
@@ -54,7 +54,7 @@ export const previewRundown = async (
|
|||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ImportMap,
|
options: ImportMap,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
rundown: OntimeRundown;
|
rundown: Rundown;
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
}> => {
|
}> => {
|
||||||
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export type OptionWithoutGroup = {
|
|||||||
withDivider?: boolean;
|
withDivider?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OptionWithGroup = {
|
type OptionWithGroup = {
|
||||||
label: string;
|
label: string;
|
||||||
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import { IoCopy } from 'react-icons/io5';
|
|||||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
import { Size } from '../../models/Util.type';
|
|
||||||
import copyToClipboard from '../../utils/copyToClipboard';
|
import copyToClipboard from '../../utils/copyToClipboard';
|
||||||
|
|
||||||
interface CopyTagProps {
|
interface CopyTagProps {
|
||||||
copyValue: string;
|
copyValue: string;
|
||||||
label: string;
|
label: string;
|
||||||
size?: Size;
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import React from 'react';
|
|||||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
|
||||||
import { runtimeStore } from '@/common/stores/runtime';
|
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
|
||||||
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
import { runtimeStore } from '../../stores/runtime';
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class ErrorBoundary extends React.Component {
|
|||||||
scope.setExtras({
|
scope.setExtras({
|
||||||
error,
|
error,
|
||||||
store: appState,
|
store: appState,
|
||||||
hasSocket: { hasConnected, shouldReconnect, reconnectAttempts },
|
hasSocket: { hasConnected, reconnectAttempts },
|
||||||
});
|
});
|
||||||
const eventId = Sentry.captureException(error);
|
const eventId = Sentry.captureException(error);
|
||||||
this.setState({ eventId, info });
|
this.setState({ eventId, info });
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
$loader-size: 4rem;
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
background-color: $black-10;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
width: $loader-size;
|
|
||||||
height: $loader-size;
|
|
||||||
background: $blue-500;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: animloader 1s ease-in infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animloader {
|
|
||||||
0% {
|
|
||||||
transform: scale(0);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animloader {
|
|
||||||
0% {
|
|
||||||
transform: scale(0);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import style from './LoaderOverlay.module.scss';
|
|
||||||
|
|
||||||
export default function LoaderOverlay() {
|
|
||||||
return (
|
|
||||||
<div className={style.overlay}>
|
|
||||||
<span className={style.loader} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import NavigationMenu from './NavigationMenu';
|
|
||||||
|
|
||||||
interface ProductionNavigationMenuProps {
|
|
||||||
isMenuOpen: boolean;
|
|
||||||
onMenuClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProductionNavigationMenu(props: ProductionNavigationMenuProps) {
|
|
||||||
const { isMenuOpen, onMenuClose } = props;
|
|
||||||
|
|
||||||
return <NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(ProductionNavigationMenu);
|
|
||||||
@@ -12,7 +12,7 @@ type OptionsField = {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MultiselectOption = { value: string; label: string; colour: string };
|
type MultiselectOption = { value: string; label: string; colour: string };
|
||||||
export type MultiselectOptions = Record<string, MultiselectOption>;
|
export type MultiselectOptions = Record<string, MultiselectOption>;
|
||||||
type MultiOptionsField = {
|
type MultiOptionsField = {
|
||||||
type: 'multi-option';
|
type: 'multi-option';
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// roughly from https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
function getMatches(query: string): boolean {
|
|
||||||
return window.matchMedia(query).matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: debounce handleChange
|
|
||||||
export default function useMediaQuery(query: string): boolean {
|
|
||||||
const [matches, setMatches] = useState<boolean>(getMatches(query));
|
|
||||||
|
|
||||||
const handleChange = useCallback(() => {
|
|
||||||
setMatches(getMatches(query));
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const matchMedia = window.matchMedia(query);
|
|
||||||
|
|
||||||
// Triggered at the first client-side load and if query changes
|
|
||||||
handleChange();
|
|
||||||
|
|
||||||
// Listen matchMedia
|
|
||||||
matchMedia.addEventListener('change', handleChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
matchMedia.removeEventListener('change', handleChange);
|
|
||||||
};
|
|
||||||
}, [handleChange, query]);
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { editAutomationSettings, getAutomationSettings } from '../api/automation';
|
import { getAutomationSettings } from '../api/automation';
|
||||||
import { AUTOMATION } from '../api/constants';
|
import { AUTOMATION } from '../api/constants';
|
||||||
import { logAxiosError } from '../api/utils';
|
|
||||||
import { automationPlaceholderSettings } from '../models/AutomationSettings';
|
import { automationPlaceholderSettings } from '../models/AutomationSettings';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
|
||||||
|
|
||||||
export default function useAutomationSettings() {
|
export default function useAutomationSettings() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
@@ -20,15 +18,3 @@ export default function useAutomationSettings() {
|
|||||||
|
|
||||||
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
|
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: '',
|
lastLoadedProject: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useProjectList() {
|
function useProjectList() {
|
||||||
const { data, status, refetch } = useQuery({
|
const { data, status, refetch } = useQuery({
|
||||||
queryKey: PROJECT_LIST,
|
queryKey: PROJECT_LIST,
|
||||||
queryFn: getProjects,
|
queryFn: getProjects,
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { NormalisedRundown, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
import { OntimeEntry, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
import { fetchNormalisedRundown } from '../api/rundown';
|
import { fetchCurrentRundown } from '../api/rundown';
|
||||||
|
|
||||||
import useProjectData from './useProjectData';
|
import useProjectData from './useProjectData';
|
||||||
|
|
||||||
// revision is -1 so that the remote revision is higher
|
// revision is -1 so that the remote revision is higher
|
||||||
const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 };
|
const cachedRundownPlaceholder: Rundown = {
|
||||||
|
id: 'default',
|
||||||
|
title: '',
|
||||||
|
order: [],
|
||||||
|
entries: {},
|
||||||
|
revision: -1,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalised rundown data
|
* Normalised rundown data
|
||||||
*/
|
*/
|
||||||
export default function useRundown() {
|
export default function useRundown() {
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||||
queryKey: RUNDOWN,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchNormalisedRundown,
|
queryFn: fetchCurrentRundown,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
@@ -37,16 +43,16 @@ export function useFlatRundown() {
|
|||||||
|
|
||||||
const loadedProject = useRef<string>('');
|
const loadedProject = useRef<string>('');
|
||||||
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
||||||
const [flatRunDown, setFlatRunDown] = useState<OntimeRundown>([]);
|
const [flatRundown, setFlatRundown] = useState<OntimeEntry[]>([]);
|
||||||
|
|
||||||
// update data whenever the revision changes
|
// update data whenever the revision changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.revision !== -1 && data.revision !== prevRevision) {
|
if (data.revision !== -1 && data.revision !== prevRevision) {
|
||||||
const flatRundown = data.order.map((id) => data.rundown[id]);
|
const flatRundown = data.order.map((id) => data.entries[id]);
|
||||||
setFlatRunDown(flatRundown);
|
setFlatRundown(flatRundown);
|
||||||
setPrevRevision(data.revision);
|
setPrevRevision(data.revision);
|
||||||
}
|
}
|
||||||
}, [data.order, data.revision, data.rundown, prevRevision]);
|
}, [data.entries, data.order, data.revision, prevRevision]);
|
||||||
|
|
||||||
// TODO: should we have a project id field?
|
// TODO: should we have a project id field?
|
||||||
// invalidate current version if project changes
|
// invalidate current version if project changes
|
||||||
@@ -57,13 +63,13 @@ export function useFlatRundown() {
|
|||||||
}
|
}
|
||||||
}, [projectData]);
|
}, [projectData]);
|
||||||
|
|
||||||
return { data: flatRunDown, status };
|
return { data: flatRundown, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to a partial rundown based on a filter callback
|
* Provides access to a partial rundown based on a filter callback
|
||||||
*/
|
*/
|
||||||
export function usePartialRundown(cb: (event: OntimeRundownEntry) => boolean) {
|
export function usePartialRundown(cb: (event: OntimeEntry) => boolean) {
|
||||||
const { data, status } = useFlatRundown();
|
const { data, status } = useFlatRundown();
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return data.filter(cb);
|
return data.filter(cb);
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { useCallback } from 'react';
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
|
MaybeString,
|
||||||
OntimeBlock,
|
OntimeBlock,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
|
OntimeEntry,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundownEntry,
|
Rundown,
|
||||||
RundownCached,
|
|
||||||
TimeField,
|
TimeField,
|
||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
TransientEventPayload,
|
TransientEventPayload,
|
||||||
@@ -31,12 +32,12 @@ import { useEditorSettings } from '../stores/editorSettings';
|
|||||||
|
|
||||||
export type EventOptions = Partial<{
|
export type EventOptions = Partial<{
|
||||||
// options to any new block (event / delay / block)
|
// options to any new block (event / delay / block)
|
||||||
after: string;
|
after: MaybeString;
|
||||||
before: string;
|
before: MaybeString;
|
||||||
// options to blocks of type OntimeEvent
|
// options to blocks of type OntimeEvent
|
||||||
defaultPublic: boolean;
|
defaultPublic: boolean;
|
||||||
linkPrevious: boolean;
|
linkPrevious: boolean;
|
||||||
lastEventId: string;
|
lastEventId: MaybeString;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,11 +58,11 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
const getEventById = useCallback(
|
const getEventById = useCallback(
|
||||||
(eventId: string) => {
|
(eventId: string) => {
|
||||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
if (!cachedRundown?.rundown) {
|
if (!cachedRundown?.entries) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return cachedRundown.rundown[eventId];
|
return cachedRundown.entries[eventId];
|
||||||
},
|
},
|
||||||
[queryClient],
|
[queryClient],
|
||||||
);
|
);
|
||||||
@@ -100,9 +101,8 @@ export const useEventAction = () => {
|
|||||||
newEvent.linkStart = applicationOptions.lastEventId;
|
newEvent.linkStart = applicationOptions.lastEventId;
|
||||||
} else if (applicationOptions?.lastEventId) {
|
} else if (applicationOptions?.lastEventId) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this is a value
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this is a value
|
||||||
const rundownData = queryClient.getQueryData<RundownCached>(RUNDOWN)!;
|
const rundownData = queryClient.getQueryData<Rundown>(RUNDOWN)!;
|
||||||
const { rundown } = rundownData;
|
const previousEvent = rundownData.entries[applicationOptions.lastEventId];
|
||||||
const previousEvent = rundown[applicationOptions.lastEventId];
|
|
||||||
if (isOntimeEvent(previousEvent)) {
|
if (isOntimeEvent(previousEvent)) {
|
||||||
newEvent.timeStart = previousEvent.timeEnd;
|
newEvent.timeStart = previousEvent.timeEnd;
|
||||||
}
|
}
|
||||||
@@ -180,15 +180,21 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
const eventId = newEvent.id;
|
const eventId = newEvent.id;
|
||||||
|
|
||||||
if (previousData && eventId) {
|
if (previousData && eventId) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const newRundown = { ...previousData.rundown };
|
const newRundown = { ...previousData.entries };
|
||||||
// @ts-expect-error -- we expect the events to be of same type
|
// @ts-expect-error -- we expect the events to be of same type
|
||||||
newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
|
newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
|
||||||
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
id: previousData.id,
|
||||||
|
title: previousData.title,
|
||||||
|
order: previousData.order,
|
||||||
|
entries: newRundown,
|
||||||
|
revision: -1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
@@ -196,7 +202,7 @@ export const useEventAction = () => {
|
|||||||
},
|
},
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _newEvent, context) => {
|
onError: (_error, _newEvent, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
@@ -210,7 +216,7 @@ export const useEventAction = () => {
|
|||||||
* Updates existing event
|
* Updates existing event
|
||||||
*/
|
*/
|
||||||
const updateEvent = useCallback(
|
const updateEvent = useCallback(
|
||||||
async (event: Partial<OntimeRundownEntry>) => {
|
async (event: Partial<OntimeEntry>) => {
|
||||||
try {
|
try {
|
||||||
await _updateEventMutation.mutateAsync(event);
|
await _updateEventMutation.mutateAsync(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -296,9 +302,9 @@ export const useEventAction = () => {
|
|||||||
* Utility function to get the previous event end time
|
* Utility function to get the previous event end time
|
||||||
*/
|
*/
|
||||||
function getPreviousEnd(): number {
|
function getPreviousEnd(): number {
|
||||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
|
||||||
if (!cachedRundown?.order || !cachedRundown?.rundown) {
|
if (!cachedRundown?.order || !cachedRundown?.entries) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +314,7 @@ export const useEventAction = () => {
|
|||||||
}
|
}
|
||||||
let previousEnd = 0;
|
let previousEnd = 0;
|
||||||
for (let i = index - 1; i >= 0; i--) {
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
const event = cachedRundown.rundown[cachedRundown.order[i]];
|
const event = cachedRundown.entries[cachedRundown.order[i]];
|
||||||
if (isOntimeEvent(event)) {
|
if (isOntimeEvent(event)) {
|
||||||
previousEnd = event.timeEnd;
|
previousEnd = event.timeEnd;
|
||||||
break;
|
break;
|
||||||
@@ -331,11 +337,11 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const previousRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
|
||||||
if (previousEvents) {
|
if (previousRundown) {
|
||||||
const eventIds = new Set(ids);
|
const eventIds = new Set(ids);
|
||||||
const newRundown = { ...previousEvents.rundown };
|
const newRundown = { ...previousRundown.entries };
|
||||||
|
|
||||||
eventIds.forEach((eventId) => {
|
eventIds.forEach((eventId) => {
|
||||||
if (Object.hasOwn(newRundown, eventId)) {
|
if (Object.hasOwn(newRundown, eventId)) {
|
||||||
@@ -349,16 +355,22 @@ export const useEventAction = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, { order: previousEvents.order, rundown: newRundown, revision: -1 });
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
id: previousRundown.id,
|
||||||
|
title: previousRundown.title,
|
||||||
|
order: previousRundown.order,
|
||||||
|
entries: newRundown,
|
||||||
|
revision: -1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous rundown
|
||||||
return { previousEvents };
|
return { previousRundown };
|
||||||
},
|
},
|
||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
onError: (_error, _newEvent, context) => {
|
onError: (_error, _newEvent, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN, context?.previousEvents);
|
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousRundown);
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -386,19 +398,21 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const newOrder = previousData.order.filter((id) => !eventIds.includes(id));
|
const newOrder = previousData.order.filter((id) => !eventIds.includes(id));
|
||||||
const newRundown = { ...previousData.rundown };
|
const newRundown = { ...previousData.entries };
|
||||||
for (const eventId of eventIds) {
|
for (const eventId of eventIds) {
|
||||||
delete newRundown[eventId];
|
delete newRundown[eventId];
|
||||||
}
|
}
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, {
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
id: previousData.id,
|
||||||
|
title: previousData.title,
|
||||||
order: newOrder,
|
order: newOrder,
|
||||||
rundown: newRundown,
|
entries: newRundown,
|
||||||
revision: -1,
|
revision: -1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -409,7 +423,7 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
@@ -445,10 +459,16 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown: {}, order: [], revision: -1 });
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
id: previousData?.id ?? 'default',
|
||||||
|
title: previousData?.title ?? '',
|
||||||
|
entries: {},
|
||||||
|
order: [],
|
||||||
|
revision: -1,
|
||||||
|
});
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousData };
|
return { previousData };
|
||||||
@@ -456,7 +476,7 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
// Mutation fails, rollback undos optimist update
|
// Mutation fails, rollback undos optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
@@ -516,13 +536,18 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const newOrder = reorderArray(previousData.order, data.from, data.to);
|
const newOrder = reorderArray(previousData.order, data.from, data.to);
|
||||||
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
queryClient.setQueryData(RUNDOWN, { order: newOrder, rundown: previousData.rundown, revision: -1 });
|
id: previousData.id,
|
||||||
|
title: previousData.title,
|
||||||
|
order: newOrder,
|
||||||
|
entries: previousData.entries,
|
||||||
|
revision: -1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
@@ -531,7 +556,7 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
@@ -572,22 +597,28 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const newRundown = { ...previousData.rundown };
|
const newRundown = { ...previousData.entries };
|
||||||
const eventA = previousData.rundown[from];
|
const eventA = previousData.entries[from];
|
||||||
const eventB = previousData.rundown[to];
|
const eventB = previousData.entries[to];
|
||||||
|
|
||||||
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { newA, newB } = swapEventData(eventA, eventB);
|
const [newA, newB] = swapEventData(eventA, eventB);
|
||||||
newRundown[from] = newA;
|
newRundown[from] = newA;
|
||||||
newRundown[to] = newB;
|
newRundown[to] = newB;
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
id: previousData.id,
|
||||||
|
title: previousData.title,
|
||||||
|
order: previousData.order,
|
||||||
|
entries: newRundown,
|
||||||
|
revision: -1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous events
|
// Return a context with the previous events
|
||||||
@@ -596,7 +627,7 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// 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>;
|
type PickFunction<T extends noop> = (this: ThisParameterType<T>, ...args: Parameters<T>) => ReturnType<T>;
|
||||||
|
|
||||||
export const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
|
const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
|
||||||
|
|
||||||
export default function useMemoisedFn<T extends noop>(fn: T) {
|
export default function useMemoisedFn<T extends noop>(fn: T) {
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
|
|||||||
@@ -91,14 +91,6 @@ export const setPlayback = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfoPanel = createSelector((state: RuntimeStore) => ({
|
|
||||||
eventNow: state.eventNow,
|
|
||||||
eventNext: state.eventNext,
|
|
||||||
playback: state.timer.playback,
|
|
||||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
|
||||||
numEvents: state.runtime.numEvents,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const useAuxTimerTime = createSelector((state: RuntimeStore) => state.auxtimer1.current);
|
export const useAuxTimerTime = createSelector((state: RuntimeStore) => state.auxtimer1.current);
|
||||||
|
|
||||||
export const useAuxTimerControl = createSelector((state: RuntimeStore) => ({
|
export const useAuxTimerControl = createSelector((state: RuntimeStore) => ({
|
||||||
@@ -145,8 +137,6 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({
|
|||||||
timeDanger: state.eventNow?.timeDanger ?? null,
|
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const setClientName = (newName: string) => socketSendJson('set-client-name', newName);
|
|
||||||
|
|
||||||
export const useRuntimeOverview = createSelector((state: RuntimeStore) => ({
|
export const useRuntimeOverview = createSelector((state: RuntimeStore) => ({
|
||||||
plannedStart: state.runtime.plannedStart,
|
plannedStart: state.runtime.plannedStart,
|
||||||
actualStart: state.runtime.actualStart,
|
actualStart: state.runtime.actualStart,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export type Size = 'xs' | 'sm' | 'md' | 'lg';
|
|
||||||
@@ -11,7 +11,7 @@ type LogStore = {
|
|||||||
logs: Log[];
|
logs: Log[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logger = createStore<LogStore>(() => ({
|
const logger = createStore<LogStore>(() => ({
|
||||||
logs: [],
|
logs: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { makeCSVFromArrayOfArrays } from '../csv';
|
import { OntimeEntry, ProjectRundowns, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
|
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../csv';
|
||||||
|
|
||||||
describe('makeCSVFromArrayOfArrays()', () => {
|
describe('makeCSVFromArrayOfArrays()', () => {
|
||||||
it('joins an array of arrays with commas and newlines', () => {
|
it('joins an array of arrays with commas and newlines', () => {
|
||||||
@@ -11,3 +13,32 @@ 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,4 +1,4 @@
|
|||||||
import { EndAction, EventCustomFields, OntimeEvent, SupportedEvent, TimerType, TimeStrategy } from 'ontime-types';
|
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEvent, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
|
|
||||||
import { cloneEvent } from '../eventsManager';
|
import { cloneEvent } from '../eventsManager';
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ describe('cloneEvent()', () => {
|
|||||||
gap: 0,
|
gap: 0,
|
||||||
custom: {
|
custom: {
|
||||||
lighting: '3',
|
lighting: '3',
|
||||||
} as EventCustomFields,
|
} as EntryCustomFields,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cloned = cloneEvent(original);
|
const cloned = cloneEvent(original);
|
||||||
|
|||||||
@@ -64,13 +64,13 @@ describe('getRouteFromPreset()', () => {
|
|||||||
describe('handle url sharing edge cases', () => {
|
describe('handle url sharing edge cases', () => {
|
||||||
it('finds the correct preset when the url contains extra arguments', () => {
|
it('finds the correct preset when the url contains extra arguments', () => {
|
||||||
const location = resolvePath('/demopage?locked=true&token=123');
|
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', () => {
|
it('appends the feature params to the alias', () => {
|
||||||
const location = resolvePath('/demopage?locked=true&token=123');
|
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,25 +83,26 @@ describe('generatePathFromPreset()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('appends the feature params to the alias', () => {
|
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()', () => {
|
describe('arePathsEquivalent()', () => {
|
||||||
it("checks whether the paths match", () => {
|
it('checks whether the paths match', () => {
|
||||||
expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy();
|
expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy();
|
||||||
expect(arePathsEquivalent('timer', 'timer')).toBeTruthy();
|
expect(arePathsEquivalent('timer', 'timer')).toBeTruthy();
|
||||||
expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).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=b')).toBeFalsy();
|
||||||
expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy();
|
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=b')).toBeFalsy();
|
||||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
|
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||||
|
import { OntimeEntry, ProjectRundowns } from 'ontime-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Converts an array of arrays to a CSV file
|
* Converts an array of arrays to a CSV file
|
||||||
* @param {string[][]} arrayOfArrays
|
|
||||||
* @return {string}
|
|
||||||
*/
|
*/
|
||||||
export function makeCSVFromArrayOfArrays(arrayOfArrays: string[][]): string {
|
export function makeCSVFromArrayOfArrays(arrayOfArrays: string[][]): string {
|
||||||
return stringify(arrayOfArrays);
|
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,6 +23,7 @@ export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
|||||||
isPublic: event.isPublic,
|
isPublic: event.isPublic,
|
||||||
skip: event.skip,
|
skip: event.skip,
|
||||||
colour: event.colour,
|
colour: event.colour,
|
||||||
|
currentBlock: event.currentBlock,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
delay: event.delay, // the events will be collocated, so having the same metadata is a good start
|
delay: event.delay, // the events will be collocated, so having the same metadata is a good start
|
||||||
dayOffset: event.dayOffset,
|
dayOffset: event.dayOffset,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Log, RundownCached, RuntimeStore } from 'ontime-types';
|
import { Log, Rundown, RuntimeStore } from 'ontime-types';
|
||||||
|
|
||||||
import { isProduction, websocketUrl } from '../../externals';
|
import { isProduction, websocketUrl } from '../../externals';
|
||||||
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants';
|
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants';
|
||||||
@@ -16,11 +16,10 @@ import { addDialog } from '../stores/dialogStore';
|
|||||||
import { addLog } from '../stores/logger';
|
import { addLog } from '../stores/logger';
|
||||||
import { addToBatchUpdates, flushBatchUpdates, patchRuntime, patchRuntimeProperty } from '../stores/runtime';
|
import { addToBatchUpdates, flushBatchUpdates, patchRuntime, patchRuntimeProperty } from '../stores/runtime';
|
||||||
|
|
||||||
export let websocket: WebSocket | null = null;
|
let websocket: WebSocket | null = null;
|
||||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
const reconnectInterval = 1000;
|
const reconnectInterval = 1000;
|
||||||
|
|
||||||
export let shouldReconnect = true;
|
|
||||||
export let hasConnected = false;
|
export let hasConnected = false;
|
||||||
export let reconnectAttempts = 0;
|
export let reconnectAttempts = 0;
|
||||||
|
|
||||||
@@ -50,15 +49,14 @@ export const connectSocket = () => {
|
|||||||
console.warn('WebSocket disconnected');
|
console.warn('WebSocket disconnected');
|
||||||
setOnlineStatus(false);
|
setOnlineStatus(false);
|
||||||
|
|
||||||
if (shouldReconnect) {
|
// we decide to allows reconnect
|
||||||
reconnectTimeout = setTimeout(() => {
|
reconnectTimeout = setTimeout(() => {
|
||||||
console.warn('WebSocket: attempting reconnect');
|
console.warn('WebSocket: attempting reconnect');
|
||||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||||
reconnectAttempts += 1;
|
reconnectAttempts += 1;
|
||||||
connectSocket();
|
connectSocket();
|
||||||
}
|
}
|
||||||
}, reconnectInterval);
|
}, reconnectInterval);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
websocket.onerror = (error) => {
|
websocket.onerror = (error) => {
|
||||||
@@ -203,7 +201,7 @@ export const connectSocket = () => {
|
|||||||
invalidateAllCaches();
|
invalidateAllCaches();
|
||||||
} else if (target === 'RUNDOWN') {
|
} else if (target === 'RUNDOWN') {
|
||||||
const { revision } = payload;
|
const { revision } = payload;
|
||||||
const currentRevision = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN)?.revision ?? -1;
|
const currentRevision = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN)?.revision ?? -1;
|
||||||
if (revision > currentRevision) {
|
if (revision > currentRevision) {
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
||||||
@@ -224,11 +222,6 @@ export const connectSocket = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const disconnectSocket = () => {
|
|
||||||
shouldReconnect = false;
|
|
||||||
websocket?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const socketSend = (message: any) => {
|
export const socketSend = (message: any) => {
|
||||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||||
websocket.send(message);
|
websocket.send(message);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function getFormatFromParams() {
|
|||||||
* Gets the format options from the applicaton settings
|
* Gets the format options from the applicaton settings
|
||||||
* @returns a string equivalent to the format, ie: hh:mm:ss a or HH:mm:ss
|
* @returns a string equivalent to the format, ie: hh:mm:ss a or HH:mm:ss
|
||||||
*/
|
*/
|
||||||
export function getFormatFromSettings(): TimeFormat {
|
function getFormatFromSettings(): TimeFormat {
|
||||||
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||||
return settings?.timeFormat ?? '24';
|
return settings?.timeFormat ?? '24';
|
||||||
}
|
}
|
||||||
|
|||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
import { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
|
|
||||||
|
|
||||||
import 'vitest';
|
|
||||||
|
|
||||||
// ugly hack because vite and pnpm are not playing ball with jest
|
|
||||||
// https://github.com/testing-library/jest-dom/issues/123
|
|
||||||
declare global {
|
|
||||||
namespace Vi {
|
|
||||||
type Assertion<T = any> = TestingLibraryMatchers<T, void>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -29,8 +29,8 @@ export default function ReportSettings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const combinedReport = useMemo(() => {
|
const combinedReport = useMemo(() => {
|
||||||
return getCombinedReport(reportData, data.rundown, data.order);
|
return getCombinedReport(reportData, data.entries, data.order);
|
||||||
}, [reportData, data.rundown, data.order]);
|
}, [reportData, data.entries, data.order]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { isOntimeEvent, MaybeNumber, NormalisedRundown, OntimeReport } from 'ontime-types';
|
import { EntryId, isOntimeEvent, MaybeNumber, OntimeReport, RundownEntries } from 'ontime-types';
|
||||||
|
|
||||||
import { makeCSVFromArrayOfArrays } from '../../../../common/utils/csv';
|
import { makeCSVFromArrayOfArrays } from '../../../../common/utils/csv';
|
||||||
import { formatTime } from '../../../../common/utils/time';
|
import { formatTime } from '../../../../common/utils/time';
|
||||||
@@ -16,7 +16,7 @@ export type CombinedReport = {
|
|||||||
/**
|
/**
|
||||||
* Creates a combined report with the rundown data
|
* Creates a combined report with the rundown data
|
||||||
*/
|
*/
|
||||||
export function getCombinedReport(report: OntimeReport, rundown: NormalisedRundown, order: string[]): CombinedReport[] {
|
export function getCombinedReport(report: OntimeReport, rundown: RundownEntries, order: EntryId[]): CombinedReport[] {
|
||||||
if (Object.keys(report).length === 0) return [];
|
if (Object.keys(report).length === 0) return [];
|
||||||
if (order.length === 0) return [];
|
if (order.length === 0) return [];
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ import * as Panel from '../../panel-utils/PanelUtils';
|
|||||||
|
|
||||||
import GeneralPinInput from './GeneralPinInput';
|
import GeneralPinInput from './GeneralPinInput';
|
||||||
|
|
||||||
export type GeneralPanelFormValues = {
|
|
||||||
filename: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function GeneralPanelForm() {
|
export default function GeneralPanelForm() {
|
||||||
const { data, status, refetch } = useSettings();
|
const { data, status, refetch } = useSettings();
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export default function EditorSettingsForm() {
|
|||||||
onChange={(event) => setDefaultEndAction(event.target.value as EndAction)}
|
onChange={(event) => setDefaultEndAction(event.target.value as EndAction)}
|
||||||
>
|
>
|
||||||
<option value={EndAction.None}>None</option>
|
<option value={EndAction.None}>None</option>
|
||||||
<option value={EndAction.Stop}>Stop</option>
|
|
||||||
<option value={EndAction.LoadNext}>Load next</option>
|
<option value={EndAction.LoadNext}>Load next</option>
|
||||||
<option value={EndAction.PlayNext}>Play next</option>
|
<option value={EndAction.PlayNext}>Play next</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export async function makeProjectPatch(data: DatabaseModel, mergeKeys: Record<st
|
|||||||
for (const key in mergeKeys) {
|
for (const key in mergeKeys) {
|
||||||
if (isKeyOfType(key, data) && mergeKeys[key]) {
|
if (isKeyOfType(key, data) && mergeKeys[key]) {
|
||||||
// if the rundown is merged we also need the custom fields
|
// if the rundown is merged we also need the custom fields
|
||||||
if (key === 'rundown') {
|
if (key === 'rundowns') {
|
||||||
patchObject.customFields = data['customFields'];
|
patchObject.customFields = data['customFields'];
|
||||||
}
|
}
|
||||||
Object.assign(patchObject, { [key]: data[key] });
|
Object.assign(patchObject, { [key]: data[key] });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
import { CustomFields, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import useGoogleSheet from './useGoogleSheet';
|
|||||||
import { useSheetStore } from './useSheetStore';
|
import { useSheetStore } from './useSheetStore';
|
||||||
|
|
||||||
interface ImportReviewProps {
|
interface ImportReviewProps {
|
||||||
rundown: OntimeRundown;
|
rundown: Rundown;
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
onFinished: () => void;
|
onFinished: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -29,7 +29,12 @@ export default function ImportReview(props: ImportReviewProps) {
|
|||||||
|
|
||||||
const applyImport = async () => {
|
const applyImport = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await importRundown(rundown, customFields);
|
await importRundown(
|
||||||
|
{
|
||||||
|
[rundown.id]: rundown,
|
||||||
|
},
|
||||||
|
customFields,
|
||||||
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ import { ImportCustom, ImportMap } from 'ontime-utils';
|
|||||||
export type NamedImportMap = typeof namedImportMap;
|
export type NamedImportMap = typeof namedImportMap;
|
||||||
|
|
||||||
// Record of label and import name
|
// Record of label and import name
|
||||||
export const namedImportMap = {
|
const namedImportMap = {
|
||||||
Worksheet: 'event schedule',
|
Worksheet: 'event schedule',
|
||||||
Start: 'time start',
|
Start: 'time start',
|
||||||
'Link start': 'link start',
|
'Link start': 'link start',
|
||||||
|
|||||||
+32
-31
@@ -1,6 +1,6 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { IoLink } from 'react-icons/io5';
|
import { IoLink } from 'react-icons/io5';
|
||||||
import { CustomFields, isOntimeBlock, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
import { CustomFields, isOntimeBlock, isOntimeEvent, Rundown } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import Tag from '../../../../../common/components/tag/Tag';
|
import Tag from '../../../../../common/components/tag/Tag';
|
||||||
@@ -10,7 +10,7 @@ import * as Panel from '../../../panel-utils/PanelUtils';
|
|||||||
import style from './PreviewRundown.module.scss';
|
import style from './PreviewRundown.module.scss';
|
||||||
|
|
||||||
interface PreviewRundownProps {
|
interface PreviewRundownProps {
|
||||||
rundown: OntimeRundown;
|
rundown: Rundown;
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,75 +53,76 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rundown.map((event) => {
|
{rundown.order.map((entryId) => {
|
||||||
if (isOntimeBlock(event)) {
|
const entry = rundown.entries[entryId];
|
||||||
|
if (isOntimeBlock(entry)) {
|
||||||
return (
|
return (
|
||||||
<tr key={event.id}>
|
<tr key={entry.id}>
|
||||||
<td className={style.center}>
|
<td className={style.center}>
|
||||||
<Tag>-</Tag>
|
<Tag>-</Tag>
|
||||||
</td>
|
</td>
|
||||||
<td className={style.center}>
|
<td className={style.center}>
|
||||||
<Tag>{event.type}</Tag>
|
<Tag>{entry.type}</Tag>
|
||||||
</td>
|
</td>
|
||||||
<td />
|
<td />
|
||||||
<td colSpan={99}>{event.title}</td>
|
<td colSpan={99}>{entry.title}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isOntimeEvent(event)) {
|
if (!isOntimeEvent(entry)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
eventIndex += 1;
|
eventIndex += 1;
|
||||||
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
const colour = entry.colour ? getAccessibleColour(entry.colour) : {};
|
||||||
const countToEnd = booleanToText(event.countToEnd);
|
const countToEnd = booleanToText(entry.countToEnd);
|
||||||
const isPublic = booleanToText(event.isPublic);
|
const isPublic = booleanToText(entry.isPublic);
|
||||||
const skip = booleanToText(event.skip);
|
const skip = booleanToText(entry.skip);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={event.id}>
|
<Fragment key={entry.id}>
|
||||||
<tr>
|
<tr>
|
||||||
<td className={style.center}>
|
<td className={style.center}>
|
||||||
<Tag>{eventIndex}</Tag>
|
<Tag>{eventIndex}</Tag>
|
||||||
</td>
|
</td>
|
||||||
<td className={style.center}>
|
<td className={style.center}>
|
||||||
<Tag>{event.type}</Tag>
|
<Tag>{entry.type}</Tag>
|
||||||
</td>
|
</td>
|
||||||
<td className={style.nowrap}>{event.cue}</td>
|
<td className={style.nowrap}>{entry.cue}</td>
|
||||||
<td>{event.title}</td>
|
<td>{entry.title}</td>
|
||||||
<td className={style.flex}>
|
<td className={style.flex}>
|
||||||
<span className={event.linkStart ? style.subdued : undefined}>{millisToString(event.timeStart)}</span>
|
<span className={entry.linkStart ? style.subdued : undefined}>{millisToString(entry.timeStart)}</span>
|
||||||
{event.linkStart && <IoLink className={style.linkStartActive} />}
|
{entry.linkStart && <IoLink className={style.linkStartActive} />}
|
||||||
</td>
|
</td>
|
||||||
<td>{millisToString(event.timeEnd)}</td>
|
<td>{millisToString(entry.timeEnd)}</td>
|
||||||
<td>{millisToString(event.duration)}</td>
|
<td>{millisToString(entry.duration)}</td>
|
||||||
<td>{millisToString(event.timeWarning)}</td>
|
<td>{millisToString(entry.timeWarning)}</td>
|
||||||
<td>{millisToString(event.timeDanger)}</td>
|
<td>{millisToString(entry.timeDanger)}</td>
|
||||||
<td className={style.center}>{countToEnd && <Tag>{countToEnd}</Tag>}</td>
|
<td className={style.center}>{countToEnd && <Tag>{countToEnd}</Tag>}</td>
|
||||||
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
||||||
<td>{skip && <Tag>{skip}</Tag>}</td>
|
<td>{skip && <Tag>{skip}</Tag>}</td>
|
||||||
<td style={{ ...colour }}>{event.colour}</td>
|
<td style={{ ...colour }}>{entry.colour}</td>
|
||||||
<td className={style.center}>
|
<td className={style.center}>
|
||||||
<Tag>{event.timerType}</Tag>
|
<Tag>{entry.timerType}</Tag>
|
||||||
</td>
|
</td>
|
||||||
<td className={style.center}>
|
<td className={style.center}>
|
||||||
<Tag>{event.endAction}</Tag>
|
<Tag>{entry.endAction}</Tag>
|
||||||
</td>
|
</td>
|
||||||
{isOntimeEvent(event) &&
|
{isOntimeEvent(entry) &&
|
||||||
fieldKeys.map((field) => {
|
fieldKeys.map((field) => {
|
||||||
let value = '';
|
let value = '';
|
||||||
if (field in event.custom) {
|
if (field in entry.custom) {
|
||||||
value = event.custom[field];
|
value = entry.custom[field];
|
||||||
}
|
}
|
||||||
return <td key={field}>{value}</td>;
|
return <td key={field}>{value}</td>;
|
||||||
})}
|
})}
|
||||||
<td className={style.center}>
|
<td className={style.center}>
|
||||||
<Tag>{event.id}</Tag>
|
<Tag>{entry.id}</Tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{event.note && (
|
{entry.note && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={99} className={style.secondaryRow}>
|
<td colSpan={99} className={style.secondaryRow}>
|
||||||
Note: {event.note}
|
Note: {entry.note}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, ProjectRundowns } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
|
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
|
||||||
@@ -75,9 +75,9 @@ export default function useGoogleSheet() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** applies rundown and customFields to current project */
|
/** applies rundown and customFields to current project */
|
||||||
const importRundown = async (rundown: OntimeRundown, customFields: CustomFields) => {
|
const importRundown = async (rundowns: ProjectRundowns, customFields: CustomFields) => {
|
||||||
try {
|
try {
|
||||||
await patchData({ rundown, customFields });
|
await patchData({ rundowns, customFields });
|
||||||
// we are unable to optimistically set the rundown since we need
|
// we are unable to optimistically set the rundown since we need
|
||||||
// it to be normalised
|
// it to be normalised
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||||
import { defaultImportMap, ImportMap } from 'ontime-utils';
|
import { defaultImportMap, ImportMap } from 'ontime-utils';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ type SheetStore = {
|
|||||||
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
||||||
|
|
||||||
// we get this from a preview response
|
// we get this from a preview response
|
||||||
rundown: OntimeRundown | null;
|
rundown: Rundown | null;
|
||||||
setRundown: (rundown: OntimeRundown | null) => void;
|
setRundown: (rundown: Rundown | null) => void;
|
||||||
|
|
||||||
// we get this from a preview response
|
// we get this from a preview response
|
||||||
customFields: CustomFields | null;
|
customFields: CustomFields | null;
|
||||||
@@ -60,7 +60,7 @@ export const useSheetStore = create<SheetStore>((set, get) => ({
|
|||||||
|
|
||||||
setAuthenticationStatus: (status: AuthenticationStatus) => set({ authenticationStatus: status }),
|
setAuthenticationStatus: (status: AuthenticationStatus) => set({ authenticationStatus: status }),
|
||||||
|
|
||||||
setRundown: (rundown: OntimeRundown | null) => set({ rundown }),
|
setRundown: (rundown: Rundown | null) => set({ rundown }),
|
||||||
|
|
||||||
setCustomFields: (customFields: CustomFields | null) => set({ customFields }),
|
setCustomFields: (customFields: CustomFields | null) => set({ customFields }),
|
||||||
|
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ export default function Operator() {
|
|||||||
let isPast = Boolean(featureData.selectedEventId);
|
let isPast = Boolean(featureData.selectedEventId);
|
||||||
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||||
|
|
||||||
const { firstEvent } = getFirstEventNormal(data.rundown, data.order);
|
const { firstEvent } = getFirstEventNormal(data.entries, data.order);
|
||||||
const { lastEvent } = getLastEventNormal(data.rundown, data.order);
|
const { lastEvent } = getLastEventNormal(data.entries, data.order);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.operatorContainer}>
|
<div className={style.operatorContainer}>
|
||||||
@@ -152,7 +152,7 @@ export default function Operator() {
|
|||||||
|
|
||||||
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
||||||
{data.order.map((eventId) => {
|
{data.order.map((eventId) => {
|
||||||
const entry = data.rundown[eventId];
|
const entry = data.entries[eventId];
|
||||||
if (isOntimeEvent(entry)) {
|
if (isOntimeEvent(entry)) {
|
||||||
const isSelected = featureData.selectedEventId === entry.id;
|
const isSelected = featureData.selectedEventId === entry.id;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-right: 4px; // size of scrollbar
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -16,6 +15,7 @@
|
|||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-inline: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useHotkeys } from '@mantine/hooks';
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
|
type EntryId,
|
||||||
|
type MaybeString,
|
||||||
|
type PlayableEvent,
|
||||||
|
type Rundown,
|
||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isPlayableEvent,
|
isPlayableEvent,
|
||||||
MaybeString,
|
|
||||||
PlayableEvent,
|
|
||||||
Playback,
|
Playback,
|
||||||
RundownCached,
|
|
||||||
SupportedEvent,
|
SupportedEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
getPreviousBlockNormal,
|
getPreviousBlockNormal,
|
||||||
getPreviousNormal,
|
getPreviousNormal,
|
||||||
isNewLatest,
|
isNewLatest,
|
||||||
|
reorderArray,
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { type EventOptions, useEventAction } from '../../common/hooks/useEventAction';
|
import { type EventOptions, useEventAction } from '../../common/hooks/useEventAction';
|
||||||
@@ -39,12 +41,12 @@ import style from './Rundown.module.scss';
|
|||||||
const RundownEntry = lazy(() => import('./RundownEntry'));
|
const RundownEntry = lazy(() => import('./RundownEntry'));
|
||||||
|
|
||||||
interface RundownProps {
|
interface RundownProps {
|
||||||
data: RundownCached;
|
data: Rundown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Rundown({ data }: RundownProps) {
|
export default function Rundown({ data }: RundownProps) {
|
||||||
const { order, rundown } = data;
|
const { order, entries } = data;
|
||||||
const [statefulEntries, setStatefulEntries] = useState(order);
|
const [statefulEntries, setStatefulEntries] = useState<EntryId[]>(order);
|
||||||
|
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
||||||
@@ -65,30 +67,30 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
const deleteAtCursor = useCallback(
|
const deleteAtCursor = useCallback(
|
||||||
(cursor: string | null) => {
|
(cursor: string | null) => {
|
||||||
if (!cursor) return;
|
if (!cursor) return;
|
||||||
const { entry, index } = getPreviousNormal(rundown, order, cursor);
|
const { entry, index } = getPreviousNormal(entries, order, cursor);
|
||||||
deleteEvent([cursor]);
|
deleteEvent([cursor]);
|
||||||
if (entry && index !== null) {
|
if (entry && index !== null) {
|
||||||
setSelectedEvents({ id: entry.id, selectMode: 'click', index });
|
setSelectedEvents({ id: entry.id, selectMode: 'click', index });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rundown, order, deleteEvent, setSelectedEvents],
|
[entries, order, deleteEvent, setSelectedEvents],
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertCopyAtId = useCallback(
|
const insertCopyAtId = useCallback(
|
||||||
(atId: string | null, copyId: string | null, above = false) => {
|
(atId: string | null, copyId: string | null, above = false) => {
|
||||||
const adjustedCursor = above ? getPreviousNormal(rundown, order, atId ?? '').entry?.id ?? null : atId;
|
const adjustedCursor = above ? getPreviousNormal(entries, order, atId ?? '').entry?.id ?? null : atId;
|
||||||
if (copyId === null) {
|
if (copyId === null) {
|
||||||
// we cant clone without selection
|
// we cant clone without selection
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cloneEntry = rundown[copyId];
|
const cloneEntry = entries[copyId];
|
||||||
if (cloneEntry?.type === SupportedEvent.Event) {
|
if (cloneEntry?.type === SupportedEvent.Event) {
|
||||||
//if we don't have a cursor add the new event on top
|
//if we don't have a cursor add the new event on top
|
||||||
const newEvent = cloneEvent(cloneEntry);
|
const newEvent = cloneEvent(cloneEntry);
|
||||||
addEvent(newEvent, { after: adjustedCursor ?? undefined });
|
addEvent(newEvent, { after: adjustedCursor ?? undefined });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addEvent, order, rundown],
|
[addEvent, order, entries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertAtId = useCallback(
|
const insertAtId = useCallback(
|
||||||
@@ -124,7 +126,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
let newCursor = cursor;
|
let newCursor = cursor;
|
||||||
if (cursor === null) {
|
if (cursor === null) {
|
||||||
// there is no cursor, we select the first or last depending on direction
|
// there is no cursor, we select the first or last depending on direction
|
||||||
const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
|
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||||
|
|
||||||
if (isOntimeBlock(selected)) {
|
if (isOntimeBlock(selected)) {
|
||||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||||
@@ -140,14 +142,14 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
// otherwise we select the next or previous
|
// otherwise we select the next or previous
|
||||||
const selected =
|
const selected =
|
||||||
direction === 'up'
|
direction === 'up'
|
||||||
? getPreviousBlockNormal(rundown, order, newCursor)
|
? getPreviousBlockNormal(entries, order, newCursor)
|
||||||
: getNextBlockNormal(rundown, order, newCursor);
|
: getNextBlockNormal(entries, order, newCursor);
|
||||||
|
|
||||||
if (selected.entry !== null && selected.index !== null) {
|
if (selected.entry !== null && selected.index !== null) {
|
||||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[order, rundown, setSelectedEvents],
|
[order, entries, setSelectedEvents],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectEntry = useCallback(
|
const selectEntry = useCallback(
|
||||||
@@ -158,7 +160,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
|
|
||||||
if (cursor === null) {
|
if (cursor === null) {
|
||||||
// there is no cursor, we select the first or last depending on direction if it exists
|
// there is no cursor, we select the first or last depending on direction if it exists
|
||||||
const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
|
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||||
if (selected !== null) {
|
if (selected !== null) {
|
||||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||||
}
|
}
|
||||||
@@ -167,13 +169,13 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
|
|
||||||
// otherwise we select the next or previous
|
// otherwise we select the next or previous
|
||||||
const selected =
|
const selected =
|
||||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
|
||||||
|
|
||||||
if (selected.entry !== null && selected.index !== null) {
|
if (selected.entry !== null && selected.index !== null) {
|
||||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[order, rundown, setSelectedEvents],
|
[order, entries, setSelectedEvents],
|
||||||
);
|
);
|
||||||
|
|
||||||
const moveEntry = useCallback(
|
const moveEntry = useCallback(
|
||||||
@@ -182,14 +184,14 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { index } =
|
const { index } =
|
||||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
|
||||||
|
|
||||||
if (index !== null) {
|
if (index !== null) {
|
||||||
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
||||||
reorderEvent(cursor, offsetIndex, index);
|
reorderEvent(cursor, offsetIndex, index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[order, reorderEvent, rundown],
|
[order, reorderEvent, entries],
|
||||||
);
|
);
|
||||||
|
|
||||||
// shortcuts
|
// shortcuts
|
||||||
@@ -238,6 +240,9 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index });
|
setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index });
|
||||||
}, [appMode, featureData.selectedEventId, order, setSelectedEvents]);
|
}, [appMode, featureData.selectedEventId, order, setSelectedEvents]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On drag end, we reorder the events
|
||||||
|
*/
|
||||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
|
|
||||||
@@ -245,9 +250,10 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
if (active.id !== over?.id) {
|
if (active.id !== over?.id) {
|
||||||
const fromIndex = active.data.current?.sortable.index;
|
const fromIndex = active.data.current?.sortable.index;
|
||||||
const toIndex = over.data.current?.sortable.index;
|
const toIndex = over.data.current?.sortable.index;
|
||||||
// ugly hack to handle inconsistencies between dnd-kit and async store updates
|
|
||||||
|
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
||||||
setStatefulEntries((currentEntries) => {
|
setStatefulEntries((currentEntries) => {
|
||||||
return arrayMove(currentEntries, fromIndex, toIndex);
|
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||||
});
|
});
|
||||||
reorderEvent(String(active.id), fromIndex, toIndex);
|
reorderEvent(String(active.id), fromIndex, toIndex);
|
||||||
}
|
}
|
||||||
@@ -259,11 +265,11 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// last event is used to calculate relative timings
|
// last event is used to calculate relative timings
|
||||||
let lastEvent: PlayableEvent | undefined; // used by indicators
|
let lastEvent: PlayableEvent | null = null; // used by indicators
|
||||||
let thisEvent: PlayableEvent | undefined;
|
let thisEvent: PlayableEvent | null = null;
|
||||||
// previous entry is used to infer position in the rundown for new events
|
// previous entry is used to infer position in the rundown for new events
|
||||||
let previousEntryId: string | undefined;
|
let previousEntryId: MaybeString = null;
|
||||||
let thisId = previousEntryId;
|
let thisId: MaybeString = null;
|
||||||
|
|
||||||
let eventIndex = 0;
|
let eventIndex = 0;
|
||||||
// all events before the current selected are in the past
|
// all events before the current selected are in the past
|
||||||
@@ -272,6 +278,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
let totalGap = 0;
|
let totalGap = 0;
|
||||||
const isEditMode = appMode === AppMode.Edit;
|
const isEditMode = appMode === AppMode.Edit;
|
||||||
let isLinkedToLoaded = true; //check if the event can link all the way back to the currently playing event
|
let isLinkedToLoaded = true; //check if the event can link all the way back to the currently playing event
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||||
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
||||||
@@ -281,7 +288,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
// we iterate through a stateful copy of order to make the operations smoother
|
// 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
|
// 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
|
// instead of writing all the logic guards, we simply short circuit rendering here
|
||||||
const entry = rundown[entryId];
|
const entry = entries[entryId];
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
import {
|
||||||
|
isOntimeBlock,
|
||||||
|
isOntimeDelay,
|
||||||
|
isOntimeEvent,
|
||||||
|
MaybeString,
|
||||||
|
OntimeEntry,
|
||||||
|
OntimeEvent,
|
||||||
|
Playback,
|
||||||
|
SupportedEvent,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||||
@@ -28,13 +37,13 @@ export type EventItemActions =
|
|||||||
interface RundownEntryProps {
|
interface RundownEntryProps {
|
||||||
type: SupportedEvent;
|
type: SupportedEvent;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
data: OntimeRundownEntry;
|
data: OntimeEntry;
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
isNextDay: boolean;
|
isNextDay: boolean;
|
||||||
previousEntryId?: string;
|
previousEntryId: MaybeString;
|
||||||
previousEventId?: string;
|
previousEventId?: string;
|
||||||
playback?: Playback; // we only care about this if this event is playing
|
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
|
isRolling: boolean; // we need to know even if not related to this event
|
||||||
@@ -150,7 +159,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.type === SupportedEvent.Event) {
|
if (isOntimeEvent(data)) {
|
||||||
return (
|
return (
|
||||||
<EventBlock
|
<EventBlock
|
||||||
eventId={data.id}
|
eventId={data.id}
|
||||||
@@ -167,7 +176,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
timerType={data.timerType}
|
timerType={data.timerType}
|
||||||
title={data.title}
|
title={data.title}
|
||||||
note={data.note}
|
note={data.note}
|
||||||
delay={data.delay ?? 0}
|
delay={data.delay}
|
||||||
colour={data.colour}
|
colour={data.colour}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
@@ -184,9 +193,15 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
actionHandler={actionHandler}
|
actionHandler={actionHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (data.type === SupportedEvent.Block) {
|
} else if (isOntimeBlock(data)) {
|
||||||
return <BlockBlock data={data} hasCursor={hasCursor} onDelete={() => actionHandler('delete')} />;
|
return (
|
||||||
} else if (data.type === SupportedEvent.Delay) {
|
<BlockBlock data={data} hasCursor={hasCursor}>
|
||||||
|
{data.events.map((eventId) => {
|
||||||
|
return <div key={eventId}>{eventId}</div>;
|
||||||
|
})}
|
||||||
|
</BlockBlock>
|
||||||
|
);
|
||||||
|
} else if (isOntimeDelay(data)) {
|
||||||
return <DelayBlock data={data} hasCursor={hasCursor} />;
|
return <DelayBlock data={data} hasCursor={hasCursor} />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
.list {
|
.list {
|
||||||
@include editor.panel;
|
@include editor.panel;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
padding-left: 0;
|
padding-inline: 0;
|
||||||
box-shadow: $box-shadow-right;
|
box-shadow: $box-shadow-right;
|
||||||
|
|
||||||
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
|
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
|
||||||
|
|||||||
@@ -21,18 +21,12 @@ $block-cursor-color: $orange-400;
|
|||||||
border: 1px solid $white-7;
|
border: 1px solid $white-7;
|
||||||
border-radius: $block-border-radius;
|
border-radius: $block-border-radius;
|
||||||
margin-block: 0.25rem;
|
margin-block: 0.25rem;
|
||||||
margin-right: 0.125rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
color: $block-text-color;
|
color: $block-text-color;
|
||||||
|
|
||||||
min-width: $block-width;
|
min-width: $block-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin block-spacing() {
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin drag-style() {
|
@mixin drag-style() {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
@use '../blockMixins' as *;
|
@use '../blockMixins' as *;
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
@include block-spacing;
|
|
||||||
@include block-styling;
|
@include block-styling;
|
||||||
margin-left: 0.5rem;
|
|
||||||
|
|
||||||
background-color: $block-bg2;
|
background-color: $block-bg2;
|
||||||
|
|
||||||
@@ -22,7 +20,3 @@
|
|||||||
.drag {
|
.drag {
|
||||||
@include drag-style;
|
@include drag-style;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionMenu {
|
|
||||||
justify-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef } from 'react';
|
import { PropsWithChildren, useRef } from 'react';
|
||||||
import { IoReorderTwo } from 'react-icons/io5';
|
import { IoReorderTwo } from 'react-icons/io5';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
@@ -7,18 +7,15 @@ import { OntimeBlock } from 'ontime-types';
|
|||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||||
|
|
||||||
import BlockDelete from './BlockDelete';
|
|
||||||
|
|
||||||
import style from './BlockBlock.module.scss';
|
import style from './BlockBlock.module.scss';
|
||||||
|
|
||||||
interface BlockBlockProps {
|
interface BlockBlockProps {
|
||||||
data: OntimeBlock;
|
data: OntimeBlock;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
onDelete: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BlockBlock(props: BlockBlockProps) {
|
export default function BlockBlock(props: PropsWithChildren<BlockBlockProps>) {
|
||||||
const { data, hasCursor, onDelete } = props;
|
const { data, hasCursor, children } = props;
|
||||||
|
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
|
|
||||||
@@ -46,7 +43,8 @@ export default function BlockBlock(props: BlockBlockProps) {
|
|||||||
<IoReorderTwo />
|
<IoReorderTwo />
|
||||||
</span>
|
</span>
|
||||||
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
||||||
<BlockDelete onDelete={onDelete} />
|
<button>+++</button>
|
||||||
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { IoTrash } from 'react-icons/io5';
|
|
||||||
import { IconButton } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
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,11 +1,10 @@
|
|||||||
@use '../blockMixins' as *;
|
@use '../blockMixins' as *;
|
||||||
|
|
||||||
.delay {
|
.delay {
|
||||||
@include block-spacing;
|
|
||||||
@include block-styling;
|
@include block-styling;
|
||||||
margin-left: 0.5rem;
|
|
||||||
|
|
||||||
background-color: $block-bg2;
|
background-color: $block-bg2;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2rem 1fr auto;
|
grid-template-columns: 2rem 1fr auto;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
IoPlay,
|
IoPlay,
|
||||||
IoPlayForward,
|
IoPlayForward,
|
||||||
IoPlaySkipForward,
|
IoPlaySkipForward,
|
||||||
IoStop,
|
|
||||||
IoTime,
|
IoTime,
|
||||||
} from 'react-icons/io5';
|
} from 'react-icons/io5';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
@@ -177,9 +176,6 @@ function EndActionIcon(props: { action: EndAction; className: string }) {
|
|||||||
if (action === EndAction.PlayNext) {
|
if (action === EndAction.PlayNext) {
|
||||||
return <IoPlayForward className={maybeActiveClasses} />;
|
return <IoPlayForward className={maybeActiveClasses} />;
|
||||||
}
|
}
|
||||||
if (action === EndAction.Stop) {
|
|
||||||
return <IoStop className={maybeActiveClasses} />;
|
|
||||||
}
|
|
||||||
return <IoPlay className={className} />;
|
return <IoPlay className={className} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,23 +15,22 @@ interface CuesheetEventEditorProps {
|
|||||||
export default function CuesheetEventEditor(props: CuesheetEventEditorProps) {
|
export default function CuesheetEventEditor(props: CuesheetEventEditorProps) {
|
||||||
const { eventId } = props;
|
const { eventId } = props;
|
||||||
const { data } = useRundown();
|
const { data } = useRundown();
|
||||||
const { order, rundown } = data;
|
|
||||||
|
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (order.length === 0) {
|
if (data.order.length === 0) {
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = rundown[eventId];
|
const event = data.entries[eventId];
|
||||||
if (event && isOntimeEvent(event)) {
|
if (event && isOntimeEvent(event)) {
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
} else {
|
} else {
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
}
|
}
|
||||||
}, [data, eventId, order, rundown]);
|
}, [eventId, data.order, data.entries]);
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import EventEditorEmpty from './EventEditorEmpty';
|
|||||||
|
|
||||||
import style from './EventEditor.module.scss';
|
import style from './EventEditor.module.scss';
|
||||||
|
|
||||||
export type EventEditorSubmitActions = keyof OntimeEvent;
|
|
||||||
|
|
||||||
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
|
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
|
||||||
|
|
||||||
interface EventEditorProps {
|
interface EventEditorProps {
|
||||||
|
|||||||
@@ -13,29 +13,28 @@ import style from './EventEditor.module.scss';
|
|||||||
export default function RundownEventEditor() {
|
export default function RundownEventEditor() {
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
const { data } = useRundown();
|
const { data } = useRundown();
|
||||||
const { order, rundown } = data;
|
|
||||||
|
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (order.length === 0) {
|
if (data.order.length === 0) {
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
|
const selectedEventId = data.order.find((entryId) => selectedEvents.has(entryId));
|
||||||
if (!selectedEventId) {
|
if (!selectedEventId) {
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const event = rundown[selectedEventId];
|
const event = data.entries[selectedEventId];
|
||||||
|
|
||||||
if (event && isOntimeEvent(event)) {
|
if (event && isOntimeEvent(event)) {
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
} else {
|
} else {
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
}
|
}
|
||||||
}, [order, rundown, selectedEvents]);
|
}, [data.order, data.entries, selectedEvents]);
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return <EventEditorEmpty />;
|
return <EventEditorEmpty />;
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ function EventEditorTimes(props: EventEditorTimesProps) {
|
|||||||
variant='ontime'
|
variant='ontime'
|
||||||
>
|
>
|
||||||
<option value={EndAction.None}>None</option>
|
<option value={EndAction.None}>None</option>
|
||||||
<option value={EndAction.Stop}>Stop rundown</option>
|
|
||||||
<option value={EndAction.LoadNext}>Load next event</option>
|
<option value={EndAction.LoadNext}>Load next event</option>
|
||||||
<option value={EndAction.PlayNext}>Play next event</option>
|
<option value={EndAction.PlayNext}>Play next event</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
padding-inline: calc(2em + 0.5rem) 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickBtn {
|
.quickBtn {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { SupportedEvent } from 'ontime-types';
|
import { MaybeString, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import { useEmitLog } from '../../../common/stores/logger';
|
import { useEmitLog } from '../../../common/stores/logger';
|
||||||
@@ -9,7 +9,7 @@ import { useEmitLog } from '../../../common/stores/logger';
|
|||||||
import style from './QuickAddBlock.module.scss';
|
import style from './QuickAddBlock.module.scss';
|
||||||
|
|
||||||
interface QuickAddBlockProps {
|
interface QuickAddBlockProps {
|
||||||
previousEventId?: string;
|
previousEventId: MaybeString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(QuickAddBlock);
|
export default memo(QuickAddBlock);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.header {
|
.header {
|
||||||
padding-inline: calc(2rem - 6px + 0.5rem) calc(1rem + 2px);
|
padding-inline: 1rem 2rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
import { isOntimeEvent, MaybeNumber, MaybeString, OntimeEvent, RundownCached } from 'ontime-types';
|
import { isOntimeEvent, MaybeNumber, MaybeString, OntimeEvent, Rundown } from 'ontime-types';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { RUNDOWN } from '../../common/api/constants';
|
import { RUNDOWN } from '../../common/api/constants';
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
import { isMacOS } from '../../common/utils/deviceUtils';
|
import { isMacOS } from '../../common/utils/deviceUtils';
|
||||||
|
|
||||||
export type SelectionMode = 'shift' | 'click' | 'ctrl';
|
type SelectionMode = 'shift' | 'click' | 'ctrl';
|
||||||
|
|
||||||
interface EventSelectionStore {
|
interface EventSelectionStore {
|
||||||
selectedEvents: Set<string>;
|
selectedEvents: Set<string>;
|
||||||
@@ -33,7 +33,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
|||||||
|
|
||||||
// on ctrl + click, we toggle the selection of that event
|
// on ctrl + click, we toggle the selection of that event
|
||||||
if (selectMode === 'ctrl') {
|
if (selectMode === 'ctrl') {
|
||||||
const rundownData = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN);
|
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
if (!rundownData) return;
|
if (!rundownData) return;
|
||||||
|
|
||||||
// if it doesnt exist, simply add to the list and set an anchor
|
// if it doesnt exist, simply add to the list and set an anchor
|
||||||
@@ -50,7 +50,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
|||||||
selectedEvents.delete(id);
|
selectedEvents.delete(id);
|
||||||
|
|
||||||
const nextIndex = rundownData.order.findIndex(
|
const nextIndex = rundownData.order.findIndex(
|
||||||
(eventId, i) => i > index && isOntimeEvent(rundownData.rundown[eventId]) && selectedEvents.has(eventId),
|
(eventId, i) => i > index && isOntimeEvent(rundownData.entries[eventId]) && selectedEvents.has(eventId),
|
||||||
);
|
);
|
||||||
|
|
||||||
// if we didnt find anything after, set the anchor to the last event
|
// if we didnt find anything after, set the anchor to the last event
|
||||||
@@ -62,13 +62,13 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
|||||||
|
|
||||||
// on shift + click, we select a range of events up to the clicked event
|
// on shift + click, we select a range of events up to the clicked event
|
||||||
if (selectMode === 'shift') {
|
if (selectMode === 'shift') {
|
||||||
const rundownData = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN);
|
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
if (!rundownData) return;
|
if (!rundownData) return;
|
||||||
|
|
||||||
// get list of rundown with only ontime events
|
// get list of rundown with only ontime events
|
||||||
const events: OntimeEvent[] = [];
|
const events: OntimeEvent[] = [];
|
||||||
rundownData.order.forEach((eventId) => {
|
rundownData.order.forEach((eventId) => {
|
||||||
const event = rundownData.rundown[eventId];
|
const event = rundownData.entries[eventId];
|
||||||
if (isOntimeEvent(event)) {
|
if (isOntimeEvent(event)) {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
// used in both sm and public views
|
|
||||||
export const titleVariants = {
|
|
||||||
hidden: {
|
|
||||||
x: -1500,
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
x: 0,
|
|
||||||
transition: {
|
|
||||||
duration: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exit: {
|
|
||||||
x: -1500,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
|
OntimeEntry,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundownEntry,
|
|
||||||
Playback,
|
Playback,
|
||||||
ProjectData,
|
ProjectData,
|
||||||
Runtime,
|
Runtime,
|
||||||
@@ -72,7 +72,7 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
}
|
}
|
||||||
if (followThis !== null) {
|
if (followThis !== null) {
|
||||||
setFollow(followThis);
|
setFollow(followThis);
|
||||||
const idx: number = backstageEvents.findIndex((event: OntimeRundownEntry) => event.id === followThis?.id);
|
const idx: number = backstageEvents.findIndex((event: OntimeEntry) => event.id === followThis?.id);
|
||||||
const delayToEvent = backstageEvents[idx]?.delay ?? 0;
|
const delayToEvent = backstageEvents[idx]?.delay ?? 0;
|
||||||
setDelay(delayToEvent);
|
setDelay(delayToEvent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
import { OntimeEntry, OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import Empty from '../../../common/components/state/Empty';
|
import Empty from '../../../common/components/state/Empty';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
@@ -10,7 +10,7 @@ import { sanitiseTitle } from './countdown.helpers';
|
|||||||
import './Countdown.scss';
|
import './Countdown.scss';
|
||||||
|
|
||||||
interface CountdownSelectProps {
|
interface CountdownSelectProps {
|
||||||
events: OntimeRundownEntry[];
|
events: OntimeEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduleFormat = { format12: 'hh:mm a', format24: 'HH:mm' };
|
const scheduleFormat = { format12: 'hh:mm a', format24: 'HH:mm' };
|
||||||
@@ -19,9 +19,7 @@ export default function CountdownSelect(props: CountdownSelectProps) {
|
|||||||
const { events } = props;
|
const { events } = props;
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
|
|
||||||
const filteredEvents = events.filter(
|
const filteredEvents = events.filter((event: OntimeEntry) => event.type === SupportedEvent.Event) as OntimeEvent[];
|
||||||
(event: OntimeRundownEntry) => event.type === SupportedEvent.Event,
|
|
||||||
) as OntimeEvent[];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='event-select' data-testid='countdown__select'>
|
<div className='event-select' data-testid='countdown__select'>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import type { MaybeString, OntimeEvent, OntimeRundown, ProjectData, Settings } from 'ontime-types';
|
import type { MaybeString, OntimeEntry, OntimeEvent, ProjectData, Settings } from 'ontime-types';
|
||||||
import { Playback } from 'ontime-types';
|
import { Playback } from 'ontime-types';
|
||||||
import { millisToString, removeSeconds, secondsInMillis } from 'ontime-utils';
|
import { millisToString, removeSeconds, secondsInMillis } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ import StudioClockSchedule from './StudioClockSchedule';
|
|||||||
import './StudioClock.scss';
|
import './StudioClock.scss';
|
||||||
|
|
||||||
interface StudioClockProps {
|
interface StudioClockProps {
|
||||||
backstageEvents: OntimeRundown;
|
backstageEvents: OntimeEntry[];
|
||||||
eventNext: OntimeEvent | null;
|
eventNext: OntimeEvent | null;
|
||||||
general: ProjectData;
|
general: ProjectData;
|
||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown } from 'ontime-types';
|
import { isOntimeEvent, MaybeString, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
@@ -8,7 +8,7 @@ import { trimRundown } from './studioClock.utils';
|
|||||||
import './StudioClock.scss';
|
import './StudioClock.scss';
|
||||||
|
|
||||||
interface StudioClockScheduleProps {
|
interface StudioClockScheduleProps {
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeEntry[];
|
||||||
selectedId: MaybeString;
|
selectedId: MaybeString;
|
||||||
nextId: MaybeString;
|
nextId: MaybeString;
|
||||||
onAir: boolean;
|
onAir: boolean;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface TranslationContextValue {
|
|||||||
getLocalizedString: (key: keyof typeof langEn, lang?: string) => string;
|
getLocalizedString: (key: keyof typeof langEn, lang?: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TranslationContext = createContext<TranslationContextValue>({
|
const TranslationContext = createContext<TranslationContextValue>({
|
||||||
getLocalizedString: () => '',
|
getLocalizedString: () => '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
import { isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export const ScheduleProvider = ({
|
|||||||
isBackstage = false,
|
isBackstage = false,
|
||||||
}: PropsWithChildren<ScheduleProviderProps>) => {
|
}: PropsWithChildren<ScheduleProviderProps>) => {
|
||||||
const { cycleInterval, stopCycle } = useScheduleOptions();
|
const { cycleInterval, stopCycle } = useScheduleOptions();
|
||||||
const { data: events } = usePartialRundown((event: OntimeRundownEntry) => {
|
const { data: events } = usePartialRundown((event: OntimeEntry) => {
|
||||||
if (isBackstage) {
|
if (isBackstage) {
|
||||||
return isOntimeEvent(event);
|
return isOntimeEvent(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEntry } from 'ontime-types';
|
||||||
|
|
||||||
import useColumnManager from '../cuesheet-table/useColumnManager';
|
import useColumnManager from '../cuesheet-table/useColumnManager';
|
||||||
|
|
||||||
interface CuesheetDndProps {
|
interface CuesheetDndProps {
|
||||||
columns: ColumnDef<OntimeRundownEntry>[];
|
columns: ColumnDef<OntimeEntry>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetDnd(props: PropsWithChildren<CuesheetDndProps>) {
|
export default function CuesheetDnd(props: PropsWithChildren<CuesheetDndProps>) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { useTableNav } from '@table-nav/react';
|
import { useTableNav } from '@table-nav/react';
|
||||||
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types';
|
import { isOntimeEvent, MaybeString, OntimeEntry, OntimeEvent, TimeField } from 'ontime-types';
|
||||||
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
||||||
@@ -16,8 +16,8 @@ import useColumnManager from './useColumnManager';
|
|||||||
import style from './CuesheetTable.module.scss';
|
import style from './CuesheetTable.module.scss';
|
||||||
|
|
||||||
interface CuesheetTableProps {
|
interface CuesheetTableProps {
|
||||||
data: OntimeRundown;
|
data: OntimeEntry[];
|
||||||
columns: ColumnDef<OntimeRundownEntry>[];
|
columns: ColumnDef<OntimeEntry>[];
|
||||||
showModal: (eventId: MaybeString) => void;
|
showModal: (eventId: MaybeString) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { MutableRefObject } from 'react';
|
import { MutableRefObject } from 'react';
|
||||||
import { RowModel, Table } from '@tanstack/react-table';
|
import { RowModel, Table } from '@tanstack/react-table';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { useSelectedEventId } from '../../../../common/hooks/useSocket';
|
import { useSelectedEventId } from '../../../../common/hooks/useSocket';
|
||||||
import { lazyEvaluate } from '../../../../common/utils/lazyEvaluate';
|
import { lazyEvaluate } from '../../../../common/utils/lazyEvaluate';
|
||||||
@@ -13,9 +13,9 @@ import DelayRow from './DelayRow';
|
|||||||
import EventRow from './EventRow';
|
import EventRow from './EventRow';
|
||||||
|
|
||||||
interface CuesheetBodyProps {
|
interface CuesheetBodyProps {
|
||||||
rowModel: RowModel<OntimeRundownEntry>;
|
rowModel: RowModel<OntimeEntry>;
|
||||||
selectedRef: MutableRefObject<HTMLTableRowElement | null>;
|
selectedRef: MutableRefObject<HTMLTableRowElement | null>;
|
||||||
table: Table<OntimeRundownEntry>;
|
table: Table<OntimeEntry>;
|
||||||
columnSizing: Record<string, number>;
|
columnSizing: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||||
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
import { useCuesheetOptions } from '../../cuesheet.options';
|
import { useCuesheetOptions } from '../../cuesheet.options';
|
||||||
@@ -10,7 +10,7 @@ import { SortableCell } from './SortableCell';
|
|||||||
import style from '../CuesheetTable.module.scss';
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
interface CuesheetHeaderProps {
|
interface CuesheetHeaderProps {
|
||||||
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
headerGroups: HeaderGroup<OntimeEntry>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { memo, MutableRefObject, useLayoutEffect, useRef, useState } from 'react
|
|||||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||||
import { flexRender, Table } from '@tanstack/react-table';
|
import { flexRender, Table } from '@tanstack/react-table';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import { OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
@@ -21,7 +21,7 @@ interface EventRowProps {
|
|||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
colour?: string;
|
colour?: string;
|
||||||
rowBgColour?: string;
|
rowBgColour?: string;
|
||||||
table: Table<OntimeRundownEntry>;
|
table: Table<OntimeEntry>;
|
||||||
/** hack to force re-rendering of the row when the column sizes change */
|
/** hack to force re-rendering of the row when the column sizes change */
|
||||||
columnSizing: Record<string, number>;
|
columnSizing: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,12 +2,12 @@ import { CSSProperties, ReactNode } from 'react';
|
|||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Header } from '@tanstack/react-table';
|
import { Header } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEntry } from 'ontime-types';
|
||||||
|
|
||||||
import styles from '../CuesheetTable.module.scss';
|
import styles from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
interface SortableCellProps {
|
interface SortableCellProps {
|
||||||
header: Header<OntimeRundownEntry, unknown>;
|
header: Header<OntimeEntry, unknown>;
|
||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry, TimeStrategy } from 'ontime-types';
|
import { CustomFields, isOntimeEvent, OntimeEntry, OntimeEvent, TimeStrategy } from 'ontime-types';
|
||||||
import { millisToString, removeSeconds } from 'ontime-utils';
|
import { millisToString, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
||||||
@@ -11,7 +11,7 @@ import MultiLineCell from './MultiLineCell';
|
|||||||
import SingleLineCell from './SingleLineCell';
|
import SingleLineCell from './SingleLineCell';
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
|
|
||||||
function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeStart({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
if (!table.options.meta) {
|
if (!table.options.meta) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unk
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeEnd({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
if (!table.options.meta) {
|
if (!table.options.meta) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unkno
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeDuration({ getValue, row, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
if (!table.options.meta) {
|
if (!table.options.meta) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeMultiLineField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
||||||
@@ -101,12 +101,12 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEnt
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
const initialValue = event[column.id as keyof OntimeEntry] ?? '';
|
||||||
|
|
||||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
return <MultiLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function LazyImage({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
||||||
@@ -124,7 +124,7 @@ function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unkno
|
|||||||
return <EditableImage initialValue={initialValue} updateValue={update} />;
|
return <EditableImage initialValue={initialValue} updateValue={update} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeSingleLineField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
||||||
@@ -138,12 +138,12 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEn
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
const initialValue = event[column.id as keyof OntimeEntry] ?? '';
|
||||||
|
|
||||||
return <SingleLineCell initialValue={initialValue} handleUpdate={update} />;
|
return <SingleLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeCustomField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
||||||
@@ -161,7 +161,7 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
|
|||||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeEntry>[] {
|
||||||
const dynamicCustomFields = Object.keys(customFields).map((key) => ({
|
const dynamicCustomFields = Object.keys(customFields).map((key) => ({
|
||||||
accessorKey: key,
|
accessorKey: key,
|
||||||
id: key,
|
id: key,
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { memo, ReactNode } from 'react';
|
import { memo, ReactNode } from 'react';
|
||||||
import { Button, Checkbox } from '@chakra-ui/react';
|
import { Button, Checkbox } from '@chakra-ui/react';
|
||||||
import { Column } from '@tanstack/react-table';
|
import { Column } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEntry } from 'ontime-types';
|
||||||
|
|
||||||
import * as Editor from '../../../../features/editors/editor-utils/EditorUtils';
|
import * as Editor from '../../../../features/editors/editor-utils/EditorUtils';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ const buttonProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface CuesheetTableSettingsProps {
|
interface CuesheetTableSettingsProps {
|
||||||
columns: Column<OntimeRundownEntry, unknown>[];
|
columns: Column<OntimeEntry, unknown>[];
|
||||||
handleResetResizing: () => void;
|
handleResetResizing: () => void;
|
||||||
handleResetReordering: () => void;
|
handleResetReordering: () => void;
|
||||||
handleClearToggles: () => void;
|
handleClearToggles: () => void;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEntry } from 'ontime-types';
|
||||||
|
|
||||||
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
|
export default function useColumnManager(columns: ColumnDef<OntimeEntry>[]) {
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
||||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||||
key: 'table-order',
|
key: 'table-order',
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
MaybeNumber,
|
MaybeNumber,
|
||||||
|
OntimeEntry,
|
||||||
OntimeEntryCommonKeys,
|
OntimeEntryCommonKeys,
|
||||||
OntimeRundown,
|
|
||||||
ProjectData,
|
ProjectData,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
@@ -32,12 +32,8 @@ export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Creates an array of arrays usable by xlsx for export
|
* @description Creates an array of arrays usable by xlsx for export
|
||||||
* @param {ProjectData} headerData
|
|
||||||
* @param {OntimeRundown} rundown
|
|
||||||
* @param {CustomFields} customFields
|
|
||||||
* @return {(string[])[]}
|
|
||||||
*/
|
*/
|
||||||
export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, customFields: CustomFields): string[][] => {
|
export const makeTable = (headerData: ProjectData, rundown: OntimeEntry[], customFields: CustomFields): string[][] => {
|
||||||
// create metadata header row
|
// create metadata header row
|
||||||
const data = [['Ontime · Rundown export']];
|
const data = [['Ontime · Rundown export']];
|
||||||
if (headerData.title) data.push([`Project title: ${headerData.title}`]);
|
if (headerData.title) data.push([`Project title: ${headerData.title}`]);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useViewportSize } from '@mantine/hooks';
|
import { useViewportSize } from '@mantine/hooks';
|
||||||
import { isOntimeEvent, isPlayableEvent, OntimeRundown } from 'ontime-types';
|
import { isOntimeEvent, isPlayableEvent, OntimeEntry } from 'ontime-types';
|
||||||
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||||
@@ -11,7 +11,7 @@ import style from './Timeline.module.scss';
|
|||||||
|
|
||||||
interface TimelineProps {
|
interface TimelineProps {
|
||||||
firstStart: number;
|
firstStart: number;
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeEntry[];
|
||||||
selectedEventId: string | null;
|
selectedEventId: string | null;
|
||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface SectionProps {
|
|||||||
|
|
||||||
export default memo(Section);
|
export default memo(Section);
|
||||||
|
|
||||||
export function Section(props: SectionProps) {
|
function Section(props: SectionProps) {
|
||||||
const { category, content, title, status } = props;
|
const { category, content, title, status } = props;
|
||||||
|
|
||||||
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { isOntimeEvent, isPlayableEvent, MaybeString, OntimeEvent, OntimeRundown, PlayableEvent } from 'ontime-types';
|
import { isOntimeEvent, isPlayableEvent, MaybeString, OntimeEntry, OntimeEvent, PlayableEvent } from 'ontime-types';
|
||||||
import {
|
import {
|
||||||
dayInMs,
|
dayInMs,
|
||||||
getEventWithId,
|
getEventWithId,
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
MILLIS_PER_HOUR,
|
MILLIS_PER_HOUR,
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { clamp } from '../../common/utils/math';
|
|
||||||
import { formatDuration } from '../../common/utils/time';
|
import { formatDuration } from '../../common/utils/time';
|
||||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
@@ -22,13 +21,6 @@ type CSSPosition = {
|
|||||||
width: number;
|
width: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the position (in %) of an element relative to a schedule
|
|
||||||
*/
|
|
||||||
export function getRelativePositionX(scheduleStart: number, scheduleEnd: number, now: number): number {
|
|
||||||
return clamp(((now - scheduleStart) / (scheduleEnd - scheduleStart)) * 100, 0, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates an absolute position of an element based on a schedule
|
* Calculates an absolute position of an element based on a schedule
|
||||||
*/
|
*/
|
||||||
@@ -95,7 +87,7 @@ interface ScopedRundownData {
|
|||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeString): ScopedRundownData {
|
export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeString): ScopedRundownData {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
@@ -110,7 +102,7 @@ export function useScopedRundown(rundown: OntimeRundown, selectedEventId: MaybeS
|
|||||||
let selectedIndex = selectedEventId ? Infinity : -1;
|
let selectedIndex = selectedEventId ? Infinity : -1;
|
||||||
let firstStart = null;
|
let firstStart = null;
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
let lastEntry: PlayableEvent | undefined;
|
let lastEntry: PlayableEvent | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < rundown.length; i++) {
|
for (let i = 0; i < rundown.length; i++) {
|
||||||
const currentEntry = rundown[i];
|
const currentEntry = rundown[i];
|
||||||
@@ -172,7 +164,7 @@ type UpcomingEvents = {
|
|||||||
/**
|
/**
|
||||||
* Returns upcoming events from current: now, next and followedBy
|
* Returns upcoming events from current: now, next and followedBy
|
||||||
*/
|
*/
|
||||||
export function getUpcomingEvents(events: OntimeRundown, selectedId: MaybeString): UpcomingEvents {
|
export function getUpcomingEvents(events: PlayableEvent[], selectedId: MaybeString): UpcomingEvents {
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return { now: null, next: null, followedBy: null };
|
return { now: null, next: null, followedBy: null };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/multer": "^1.4.11",
|
"@types/multer": "^1.4.11",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { authenticateSocket } from '../middleware/authenticate.js';
|
|||||||
|
|
||||||
let instance: SocketServer | null = null;
|
let instance: SocketServer | null = null;
|
||||||
|
|
||||||
export class SocketServer implements IAdapter {
|
class SocketServer implements IAdapter {
|
||||||
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
|
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
|
||||||
|
|
||||||
private wss: WebSocketServer | null;
|
private wss: WebSocketServer | null;
|
||||||
@@ -102,7 +102,7 @@ export class SocketServer implements IAdapter {
|
|||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error -- ??
|
// @ts-expect-error -- this works fine
|
||||||
const message = JSON.parse(data);
|
const message = JSON.parse(data);
|
||||||
const { type, payload } = message;
|
const { type, payload } = message;
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ export class SocketServer implements IAdapter {
|
|||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'client-name',
|
type: 'client-name',
|
||||||
payload: this.clients.get(clientId).name,
|
payload: this.getOrCreateClient(clientId),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -136,7 +136,7 @@ export class SocketServer implements IAdapter {
|
|||||||
|
|
||||||
if (type === 'set-client-type') {
|
if (type === 'set-client-type') {
|
||||||
if (payload && typeof payload == 'string') {
|
if (payload && typeof payload == 'string') {
|
||||||
const previousData = this.clients.get(clientId);
|
const previousData = this.getOrCreateClient(clientId);
|
||||||
this.clients.set(clientId, { ...previousData, type: payload });
|
this.clients.set(clientId, { ...previousData, type: payload });
|
||||||
}
|
}
|
||||||
this.sendClientList();
|
this.sendClientList();
|
||||||
@@ -145,7 +145,7 @@ export class SocketServer implements IAdapter {
|
|||||||
|
|
||||||
if (type === 'set-client-path') {
|
if (type === 'set-client-path') {
|
||||||
if (payload && typeof payload == 'string') {
|
if (payload && typeof payload == 'string') {
|
||||||
const previousData = this.clients.get(clientId);
|
const previousData = this.getOrCreateClient(clientId);
|
||||||
previousData.path = payload;
|
previousData.path = payload;
|
||||||
this.clients.set(clientId, previousData);
|
this.clients.set(clientId, previousData);
|
||||||
|
|
||||||
@@ -166,13 +166,13 @@ export class SocketServer implements IAdapter {
|
|||||||
|
|
||||||
if (type === 'set-client-name') {
|
if (type === 'set-client-name') {
|
||||||
if (payload) {
|
if (payload) {
|
||||||
const previousData = this.clients.get(clientId);
|
const previousData = this.getOrCreateClient(clientId);
|
||||||
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${payload}`);
|
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${payload}`);
|
||||||
this.clients.set(clientId, { ...previousData, name: payload });
|
this.clients.set(clientId, { ...previousData, name: payload });
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'client-name',
|
type: 'client-name',
|
||||||
payload: this.clients.get(clientId).name,
|
payload: this.getOrCreateClient(clientId).name,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -215,6 +215,19 @@ export class SocketServer implements IAdapter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getOrCreateClient(clientId: string): Client {
|
||||||
|
if (!this.clients.has(clientId)) {
|
||||||
|
this.clients.set(clientId, {
|
||||||
|
type: 'unknown',
|
||||||
|
identify: false,
|
||||||
|
name: getRandomName(),
|
||||||
|
origin: '',
|
||||||
|
path: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.clients.get(clientId) as Client;
|
||||||
|
}
|
||||||
|
|
||||||
private sendClientList(): void {
|
private sendClientList(): void {
|
||||||
const payload = Object.fromEntries(this.clients.entries());
|
const payload = Object.fromEntries(this.clients.entries());
|
||||||
this.sendAsJson({ type: 'client-list', payload });
|
this.sendAsJson({ type: 'client-list', payload });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @param {string} value - value to assign
|
* @param {string} value - value to assign
|
||||||
* @returns {object | string | null} nested object or null if no object was created
|
* @returns {object | string | null} nested object or null if no object was created
|
||||||
*/
|
*/
|
||||||
export const integrationPayloadFromPath = (path: string[], value?: unknown): object | string | null => {
|
export function integrationPayloadFromPath(path: string[], value?: unknown): object | string | null {
|
||||||
if (path.length === 1) {
|
if (path.length === 1) {
|
||||||
const key = path[0];
|
const key = path[0];
|
||||||
return value === undefined ? key : { [key]: value };
|
return value === undefined ? key : { [key]: value };
|
||||||
@@ -16,4 +16,4 @@ export const integrationPayloadFromPath = (path: string[], value?: unknown): obj
|
|||||||
const obj = shortenedPath.reduceRight((result, key) => ({ [key]: result }), parsedValue);
|
const obj = shortenedPath.reduceRight((result, key) => ({ [key]: result }), parsedValue);
|
||||||
|
|
||||||
return typeof obj === 'object' ? obj : null;
|
return typeof obj === 'object' ? obj : null;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -157,7 +157,12 @@ async function saveChanges(patch: Partial<AutomationSettings>) {
|
|||||||
const automation = getDataProvider().getAutomation();
|
const automation = getDataProvider().getAutomation();
|
||||||
|
|
||||||
// remove undefined keys from object, we probably want a better solution
|
// remove undefined keys from object, we probably want a better solution
|
||||||
Object.keys(patch).forEach((key) => (patch[key] === undefined ? delete patch[key] : {}));
|
Object.keys(patch).forEach((key) => {
|
||||||
|
const typedKey = key as keyof AutomationSettings;
|
||||||
|
if (patch[typedKey] === undefined) {
|
||||||
|
delete patch[typedKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
await getDataProvider().setAutomation({ ...automation, ...patch });
|
await getDataProvider().setAutomation({ ...automation, ...patch });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import * as projectService from '../../services/project-service/ProjectService.j
|
|||||||
|
|
||||||
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
|
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
const { rundown, project, settings, viewSettings, urlPresets, customFields, automation } = req.body;
|
const { rundowns, project, settings, viewSettings, urlPresets, customFields, automation } = req.body;
|
||||||
const patchDb: DatabaseModel = {
|
const patchDb: DatabaseModel = {
|
||||||
rundown,
|
rundowns,
|
||||||
project,
|
project,
|
||||||
settings,
|
settings,
|
||||||
viewSettings,
|
viewSettings,
|
||||||
@@ -90,7 +90,7 @@ export async function quickProjectFile(req: Request, res: Response<{ filename: s
|
|||||||
*/
|
*/
|
||||||
export async function currentProjectDownload(_req: Request, res: Response) {
|
export async function currentProjectDownload(_req: Request, res: Response) {
|
||||||
const { filename, pathToFile } = await projectService.getCurrentProject();
|
const { filename, pathToFile } = await projectService.getCurrentProject();
|
||||||
res.download(pathToFile, filename, (error) => {
|
res.download(pathToFile, filename, (error: Error | null) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
res.status(500).send({ message });
|
res.status(500).send({ message });
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const filterImageFile = (_req: Request, file: Express.Multer.File, cb: FileFilte
|
|||||||
} else {
|
} else {
|
||||||
cb(null, false);
|
cb(null, false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Build multer uploader for a single file
|
// Build multer uploader for a single file
|
||||||
export const uploadProjectFile = multer({
|
export const uploadProjectFile = multer({
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const validatePatchProject = [
|
|||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
|
|
||||||
body('rundown').isArray().optional({ nullable: false }),
|
body('rundowns').isObject().optional({ nullable: false }),
|
||||||
body('project').isObject().optional({ nullable: false }),
|
body('project').isObject().optional({ nullable: false }),
|
||||||
body('settings').isObject().optional({ nullable: false }),
|
body('settings').isObject().optional({ nullable: false }),
|
||||||
body('viewSettings').isObject().optional({ nullable: false }),
|
body('viewSettings').isObject().optional({ nullable: false }),
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { generateRundownPreview, listWorksheets, saveExcelFile } from './excel.service.js';
|
import { generateRundownPreview, listWorksheets, saveExcelFile } from './excel.service.js';
|
||||||
|
import { CustomFields, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
export async function postExcel(req: Request, res: Response) {
|
export async function postExcel(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const filePath = req.file.path;
|
// file has been validated by middleware
|
||||||
|
const filePath = (req.file as Express.Multer.File).path;
|
||||||
await saveExcelFile(filePath);
|
await saveExcelFile(filePath);
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -29,7 +31,10 @@ export async function getWorksheets(req: Request, res: Response) {
|
|||||||
* parses an Excel spreadsheet
|
* parses an Excel spreadsheet
|
||||||
* @returns parsed result
|
* @returns parsed result
|
||||||
*/
|
*/
|
||||||
export async function previewExcel(req: Request, res: Response) {
|
export async function previewExcel(
|
||||||
|
req: Request,
|
||||||
|
res: Response<{ rundown: Rundown; customFields: CustomFields } | { message: string }>,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { options } = req.body;
|
const { options } = req.body;
|
||||||
const data = generateRundownPreview(options);
|
const data = generateRundownPreview(options);
|
||||||
|
|||||||
@@ -12,5 +12,3 @@ export const router = express.Router();
|
|||||||
router.post('/upload', uploadExcel, validateFileExists, postExcel);
|
router.post('/upload', uploadExcel, validateFileExists, postExcel);
|
||||||
router.get('/worksheets', getWorksheets);
|
router.get('/worksheets', getWorksheets);
|
||||||
router.post('/preview', validateImportMapOptions, previewExcel);
|
router.post('/preview', validateImportMapOptions, previewExcel);
|
||||||
|
|
||||||
// TODO: validate import map
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
* Google Sheets
|
* Google Sheets
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
import { CustomFields, Rundown } from 'ontime-types';
|
||||||
import type { ImportMap } from 'ontime-utils';
|
import { type ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
@@ -12,7 +12,7 @@ import xlsx from 'xlsx';
|
|||||||
import type { WorkBook } from 'xlsx';
|
import type { WorkBook } from 'xlsx';
|
||||||
|
|
||||||
import { parseExcel } from '../../utils/parser.js';
|
import { parseExcel } from '../../utils/parser.js';
|
||||||
import { parseRundown } from '../../utils/parserFunctions.js';
|
import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js';
|
||||||
import { deleteFile } from '../../utils/parserUtils.js';
|
import { deleteFile } from '../../utils/parserUtils.js';
|
||||||
import { getCustomFields } from '../../services/rundown-service/rundownCache.js';
|
import { getCustomFields } from '../../services/rundown-service/rundownCache.js';
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export function listWorksheets(): string[] {
|
|||||||
return excelData.SheetNames;
|
return excelData.SheetNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateRundownPreview(options: ImportMap): { rundown: OntimeRundown; customFields: CustomFields } {
|
export function generateRundownPreview(options: ImportMap): { rundown: Rundown; customFields: CustomFields } {
|
||||||
const data = excelData.Sheets[options.worksheet];
|
const data = excelData.Sheets[options.worksheet];
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -43,15 +43,17 @@ export function generateRundownPreview(options: ImportMap): { rundown: OntimeRun
|
|||||||
|
|
||||||
const arrayOfData: unknown[][] = xlsx.utils.sheet_to_json(data, { header: 1, blankrows: false, raw: false });
|
const arrayOfData: unknown[][] = xlsx.utils.sheet_to_json(data, { header: 1, blankrows: false, raw: false });
|
||||||
|
|
||||||
const dataFromExcel = parseExcel(arrayOfData, getCustomFields(), options);
|
const dataFromExcel = parseExcel(arrayOfData, getCustomFields(), options.worksheet, options);
|
||||||
|
const parsedCustomFields = parseCustomFields(dataFromExcel);
|
||||||
|
|
||||||
// we run the parsed data through an extra step to ensure the objects shape
|
// we run the parsed data through an extra step to ensure the objects shape
|
||||||
const { rundown, customFields } = parseRundown(dataFromExcel);
|
const Rundown = parseRundown(dataFromExcel.rundown, parsedCustomFields);
|
||||||
if (rundown.length === 0) {
|
if (Rundown.order.length === 0) {
|
||||||
throw new Error(`Could not find data to import in the worksheet: ${options.worksheet}`);
|
throw new Error(`Could not find data to import in the worksheet: ${options.worksheet}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear the data
|
// clear the data
|
||||||
excelData = xlsx.utils.book_new();
|
excelData = xlsx.utils.book_new();
|
||||||
|
|
||||||
return { rundown, customFields };
|
return { rundown: Rundown, customFields: parsedCustomFields };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,5 @@ export function triggerReportEntry(
|
|||||||
sendRefetch({
|
sendRefetch({
|
||||||
target: 'REPORT',
|
target: 'REPORT',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { ErrorResponse, MessageResponse, OntimeEntry, ProjectRundownsList, Rundown } from 'ontime-types';
|
||||||
ErrorResponse,
|
|
||||||
MessageResponse,
|
|
||||||
OntimeRundown,
|
|
||||||
OntimeRundownEntry,
|
|
||||||
RundownCached,
|
|
||||||
RundownPaginated,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { getErrorMessage } from 'ontime-utils';
|
import { getErrorMessage } from 'ontime-utils';
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
@@ -21,24 +14,19 @@ import {
|
|||||||
reorderEvent,
|
reorderEvent,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
} from '../../services/rundown-service/RundownService.js';
|
} from '../../services/rundown-service/RundownService.js';
|
||||||
import {
|
import { getEventWithId, getCurrentRundown } from '../../services/rundown-service/rundownUtils.js';
|
||||||
getEventWithId,
|
|
||||||
getNormalisedRundown,
|
|
||||||
getPaginated,
|
|
||||||
getRundown,
|
|
||||||
} from '../../services/rundown-service/rundownUtils.js';
|
|
||||||
|
|
||||||
export async function rundownGetAll(_req: Request, res: Response<OntimeRundown>) {
|
export async function rundownGetAll(_req: Request, res: Response<ProjectRundownsList>) {
|
||||||
const rundown = getRundown();
|
const rundown = getCurrentRundown();
|
||||||
res.json(rundown);
|
res.json([{ id: rundown.id, title: rundown.title, numEntries: rundown.order.length, revision: rundown.revision }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rundownGetNormalised(_req: Request, res: Response<RundownCached>) {
|
export async function rundownGetCurrent(_req: Request, res: Response<Rundown>) {
|
||||||
const cachedRundown = getNormalisedRundown();
|
const cachedRundown = getCurrentRundown();
|
||||||
res.json(cachedRundown);
|
res.json(cachedRundown);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rundownGetById(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
export async function rundownGetById(req: Request, res: Response<OntimeEntry | ErrorResponse>) {
|
||||||
const { eventId } = req.params;
|
const { eventId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -55,35 +43,7 @@ export async function rundownGetById(req: Request, res: Response<OntimeRundownEn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rundownGetPaginated(req: Request, res: Response<RundownPaginated | ErrorResponse>) {
|
export async function rundownPost(req: Request, res: Response<OntimeEntry | ErrorResponse>) {
|
||||||
const { limit, offset } = req.query;
|
|
||||||
|
|
||||||
if (limit == null && offset == null) {
|
|
||||||
return res.json({
|
|
||||||
rundown: getRundown(),
|
|
||||||
total: getRundown().length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let parsedOffset = Number(offset);
|
|
||||||
if (Number.isNaN(parsedOffset)) {
|
|
||||||
parsedOffset = 0;
|
|
||||||
}
|
|
||||||
let parsedLimit = Number(limit);
|
|
||||||
if (Number.isNaN(parsedLimit)) {
|
|
||||||
parsedLimit = Infinity;
|
|
||||||
}
|
|
||||||
const paginatedRundown = getPaginated(parsedOffset, parsedLimit);
|
|
||||||
|
|
||||||
res.status(200).json(paginatedRundown);
|
|
||||||
} catch (error) {
|
|
||||||
const message = getErrorMessage(error);
|
|
||||||
res.status(400).json({ message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rundownPost(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -97,7 +57,7 @@ export async function rundownPost(req: Request, res: Response<OntimeRundownEntry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rundownPut(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
export async function rundownPut(req: Request, res: Response<OntimeEntry | ErrorResponse>) {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -126,7 +86,7 @@ export async function rundownBatchPut(req: Request, res: Response<MessageRespons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rundownReorder(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
export async function rundownReorder(req: Request, res: Response<OntimeEntry | ErrorResponse>) {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
rundownDelete,
|
rundownDelete,
|
||||||
rundownGetAll,
|
rundownGetAll,
|
||||||
rundownGetById,
|
rundownGetById,
|
||||||
rundownGetNormalised,
|
rundownGetCurrent,
|
||||||
rundownGetPaginated,
|
|
||||||
rundownPost,
|
rundownPost,
|
||||||
rundownPut,
|
rundownPut,
|
||||||
rundownReorder,
|
rundownReorder,
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
paramsMustHaveEventId,
|
paramsMustHaveEventId,
|
||||||
rundownArrayOfIds,
|
rundownArrayOfIds,
|
||||||
rundownBatchPutValidator,
|
rundownBatchPutValidator,
|
||||||
rundownGetPaginatedQueryParams,
|
|
||||||
rundownPostValidator,
|
rundownPostValidator,
|
||||||
rundownPutValidator,
|
rundownPutValidator,
|
||||||
rundownReorderValidator,
|
rundownReorderValidator,
|
||||||
@@ -27,9 +25,8 @@ import {
|
|||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
router.get('/', rundownGetAll); // not used in Ontime frontend
|
router.get('/', rundownGetAll);
|
||||||
router.get('/paginated', rundownGetPaginatedQueryParams, rundownGetPaginated); // not used in Ontime frontend
|
router.get('/current', rundownGetCurrent);
|
||||||
router.get('/normalised', rundownGetNormalised);
|
|
||||||
router.get('/:eventId', paramsMustHaveEventId, rundownGetById); // not used in Ontime frontend
|
router.get('/:eventId', paramsMustHaveEventId, rundownGetById); // not used in Ontime frontend
|
||||||
|
|
||||||
router.post('/', rundownPostValidator, rundownPost);
|
router.post('/', rundownPostValidator, rundownPost);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { body, param, query, validationResult } from 'express-validator';
|
import { body, param, validationResult } from 'express-validator';
|
||||||
import type { Request, Response, NextFunction } from 'express';
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
export const rundownPostValidator = [
|
export const rundownPostValidator = [
|
||||||
@@ -77,14 +77,3 @@ export const rundownArrayOfIds = [
|
|||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const rundownGetPaginatedQueryParams = [
|
|
||||||
query('offset').isNumeric().optional(),
|
|
||||||
query('limit').isNumeric().optional(),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
|||||||
import { publicDir } from '../../setup/index.js';
|
import { publicDir } from '../../setup/index.js';
|
||||||
import { socket } from '../../adapters/WebsocketAdapter.js';
|
import { socket } from '../../adapters/WebsocketAdapter.js';
|
||||||
import { getLastRequest } from '../../api-integration/integration.controller.js';
|
import { getLastRequest } from '../../api-integration/integration.controller.js';
|
||||||
import { getLastLoadedProject } from '../../services/app-state-service/AppStateService.js';
|
import { getCurrentProject } from '../../services/project-service/ProjectService.js';
|
||||||
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
|
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
|
||||||
import { getNetworkInterfaces } from '../../utils/network.js';
|
import { getNetworkInterfaces } from '../../utils/network.js';
|
||||||
import { getTimezoneLabel } from '../../utils/time.js';
|
import { getTimezoneLabel } from '../../utils/time.js';
|
||||||
@@ -17,7 +17,7 @@ const startedAt = new Date();
|
|||||||
export async function getSessionStats(): Promise<SessionStats> {
|
export async function getSessionStats(): Promise<SessionStats> {
|
||||||
const { connectedClients, lastConnection } = socket.getStats();
|
const { connectedClients, lastConnection } = socket.getStats();
|
||||||
const lastRequest = getLastRequest();
|
const lastRequest = getLastRequest();
|
||||||
const projectName = await getLastLoadedProject();
|
const { filename } = await getCurrentProject();
|
||||||
const { playback } = runtimeService.getRuntimeState();
|
const { playback } = runtimeService.getRuntimeState();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -25,7 +25,7 @@ export async function getSessionStats(): Promise<SessionStats> {
|
|||||||
connectedClients,
|
connectedClients,
|
||||||
lastConnection: lastConnection !== null ? lastConnection.toISOString() : null,
|
lastConnection: lastConnection !== null ? lastConnection.toISOString() : null,
|
||||||
lastRequest: lastRequest !== null ? lastRequest.toISOString() : null,
|
lastRequest: lastRequest !== null ? lastRequest.toISOString() : null,
|
||||||
projectName,
|
projectName: filename,
|
||||||
playback,
|
playback,
|
||||||
timezone: getTimezoneLabel(startedAt),
|
timezone: getTimezoneLabel(startedAt),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
import type { AuthenticationStatus, CustomFields, ErrorResponse, OntimeRundown } from 'ontime-types';
|
import type { AuthenticationStatus, CustomFields, ErrorResponse, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
import { deleteFile } from '../../utils/parserUtils.js';
|
import { deleteFile } from '../../utils/parserUtils.js';
|
||||||
import {
|
import {
|
||||||
@@ -25,10 +25,11 @@ export async function requestConnection(
|
|||||||
res: Response<{ verification_url: string; user_code: string } | ErrorResponse>,
|
res: Response<{ verification_url: string; user_code: string } | ErrorResponse>,
|
||||||
) {
|
) {
|
||||||
const { sheetId } = req.params;
|
const { sheetId } = req.params;
|
||||||
const file = req.file.path;
|
// the check for the file is done in the validation middleware
|
||||||
|
const filePath = (req.file as Express.Multer.File).path;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = readFileSync(file, 'utf-8');
|
const client = readFileSync(filePath, 'utf-8');
|
||||||
const clientSecret = handleClientSecret(client);
|
const clientSecret = handleClientSecret(client);
|
||||||
const { verification_url, user_code } = await handleInitialConnection(clientSecret, sheetId);
|
const { verification_url, user_code } = await handleInitialConnection(clientSecret, sheetId);
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export async function requestConnection(
|
|||||||
|
|
||||||
// delete uploaded file after parsing
|
// delete uploaded file after parsing
|
||||||
try {
|
try {
|
||||||
deleteFile(file);
|
await deleteFile(filePath);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
/** we dont handle failure here */
|
/** we dont handle failure here */
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,7 @@ export async function readFromSheet(
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response<
|
res: Response<
|
||||||
| {
|
| {
|
||||||
rundown: OntimeRundown;
|
rundown: Rundown;
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
}
|
}
|
||||||
| ErrorResponse
|
| ErrorResponse
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export const validateRequestConnection = [
|
|||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
// check that the file exists
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(422).json({ errors: 'File not found' });
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function postUrlPresets(req: Request, res: Response<URLPreset[] | E
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const newPresets: URLPreset[] = req.body.map((preset) => ({
|
const newPresets: URLPreset[] = req.body.map((preset: URLPreset) => ({
|
||||||
enabled: preset.enabled,
|
enabled: preset.enabled,
|
||||||
alias: preset.alias,
|
alias: preset.alias,
|
||||||
pathAndParams: preset.pathAndParams,
|
pathAndParams: preset.pathAndParams,
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { handleLegacyMessageConversion } from '../integration.legacy.js';
|
|
||||||
|
|
||||||
describe('handleLegacyConversion', () => {
|
|
||||||
it('should return the payload as is if it is not a legacy message', () => {
|
|
||||||
expect(handleLegacyMessageConversion({})).toEqual({});
|
|
||||||
const newPayload = {
|
|
||||||
timer: {
|
|
||||||
text: 'text',
|
|
||||||
visible: true,
|
|
||||||
blink: true,
|
|
||||||
blackout: true,
|
|
||||||
},
|
|
||||||
external: 'text',
|
|
||||||
};
|
|
||||||
expect(handleLegacyMessageConversion(newPayload)).toEqual(newPayload);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert a legacy payload with external message', () => {
|
|
||||||
expect(handleLegacyMessageConversion({ external: { text: 'text', visible: true } })).toEqual({
|
|
||||||
external: 'text',
|
|
||||||
timer: {
|
|
||||||
secondarySource: 'external',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleLegacyMessageConversion({ external: { visible: true } })).toEqual({
|
|
||||||
timer: {
|
|
||||||
secondarySource: 'external',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleLegacyMessageConversion({ external: { text: 'text' } })).toEqual({
|
|
||||||
external: 'text',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -16,8 +16,6 @@ import { socket } from '../adapters/WebsocketAdapter.js';
|
|||||||
import { throttle } from '../utils/throttle.js';
|
import { throttle } from '../utils/throttle.js';
|
||||||
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
|
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
|
||||||
|
|
||||||
import { handleLegacyMessageConversion } from './integration.legacy.js';
|
|
||||||
|
|
||||||
const throttledUpdateEvent = throttle(updateEvent, 20);
|
const throttledUpdateEvent = throttle(updateEvent, 20);
|
||||||
let lastRequest: Date | null = null;
|
let lastRequest: Date | null = null;
|
||||||
|
|
||||||
@@ -89,12 +87,9 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
message: (payload) => {
|
message: (payload) => {
|
||||||
assert.isObject(payload);
|
assert.isObject(payload);
|
||||||
|
|
||||||
// TODO: remove this once we feel its been enough time, ontime 3.6.0, 20/09/2024
|
|
||||||
const migratedPayload = handleLegacyMessageConversion(payload);
|
|
||||||
|
|
||||||
const patch: DeepPartial<MessageState> = {
|
const patch: DeepPartial<MessageState> = {
|
||||||
timer: 'timer' in migratedPayload ? validateTimerMessage(migratedPayload.timer) : undefined,
|
timer: 'timer' in payload ? validateTimerMessage(payload.timer) : undefined,
|
||||||
external: 'external' in migratedPayload ? validateMessage(migratedPayload.external) : undefined,
|
external: 'external' in payload ? validateMessage(payload.external) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newMessage = messageService.patch(patch);
|
const newMessage = messageService.patch(patch);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user