mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-05 14:29:20 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 197042e630 | |||
| d1780aa98c | |||
| 87b1721b9a | |||
| 9c08f0dcb1 | |||
| 0ac98f0f02 | |||
| 261cd25bec | |||
| 8cfbcd35cd | |||
| c4435259c5 | |||
| 84a7b1c1f7 | |||
| db9444aaa4 | |||
| fc007cf124 | |||
| e95685d366 | |||
| 5f634bb48a | |||
| 2af13b340b |
@@ -37,31 +37,32 @@ jobs:
|
|||||||
run: pnpm build:docker
|
run: pnpm build:docker
|
||||||
|
|
||||||
- name: Docker Login
|
- name: Docker Login
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Docker Setup Buildx
|
- name: Docker Setup Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
- name: Setup Docker metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
|
||||||
with:
|
|
||||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/ontime
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern=v{{version}}
|
|
||||||
type=semver,pattern=v{{major}}
|
|
||||||
type=raw,value=nightly,enable=${{ github.event.release.prerelease == true }}
|
|
||||||
|
|
||||||
- name: Build and push stable release
|
- name: Build and push stable release
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
if: github.event.release.prerelease == false
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
# Push is a shorthand for --output=type=registry
|
# Push is a shorthand for --output=type=registry
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
- name: Build and push pre-release
|
||||||
|
if: github.event.release.prerelease == true
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
# Push is a shorthand for --output=type=registry
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:nightly
|
||||||
|
|||||||
@@ -10,15 +10,13 @@
|
|||||||
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="32"></a>
|
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="32"></a>
|
||||||
|
|
||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe">Windows</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe">Windows</a>
|
||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">macOS (Apple Silicon)</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">MacOS Arm</a>
|
||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">macOS (Intel)</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">MacOS Intel</a>
|
||||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-x86_64.AppImage">Linux (Intel / AMD 64-bit)</a>
|
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage">Linux</a>
|
||||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-arm64.AppImage">Linux (ARM 64-bit, Raspberry Pi 4+)</a>
|
|
||||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-armv7l.AppImage">Linux (ARM 32-bit, older Raspberry Pi)</a>
|
|
||||||
|
|
||||||
... or
|
... or
|
||||||
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
||||||
- Install from <a href="https://www.npmjs.com/package/@getontime/cli">NPM</a>
|
- Install from <a href="https://www.npmjs.com/package/ontime">NPM</a>
|
||||||
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
||||||
|
|
||||||
## Need help?
|
## Need help?
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "4.4.2",
|
"version": "4.4.0-beta.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "4.4.2",
|
"version": "4.4.0-beta.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import { Tooltip } from '@base-ui/react/tooltip';
|
|||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
// apply global axios config defaults
|
|
||||||
import './common/api/axios.config';
|
|
||||||
|
|
||||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||||
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import { TranslationObject } from 'ontime-types';
|
|||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
|
||||||
import { apiEntryUrl, customTranslationsURL, TRANSLATION } from './constants';
|
import { apiEntryUrl, customTranslationsURL, TRANSLATION } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const assetsPath = `${apiEntryUrl}/assets`;
|
const assetsPath = `${apiEntryUrl}/assets`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to get css contents
|
* HTTP request to get css contents
|
||||||
*/
|
*/
|
||||||
export async function getCSSContents(options?: RequestOptions): Promise<string> {
|
export async function getCSSContents(): Promise<string> {
|
||||||
const res = await axios.get(`${assetsPath}/css`, { signal: options?.signal });
|
const res = await axios.get(`${assetsPath}/css`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +35,8 @@ export async function restoreCSSContents(): Promise<string> {
|
|||||||
/**
|
/**
|
||||||
* HTTP request to get user translation
|
* HTTP request to get user translation
|
||||||
*/
|
*/
|
||||||
export async function getUserTranslation(options?: RequestOptions): Promise<TranslationObject> {
|
export async function getUserTranslation(): Promise<TranslationObject> {
|
||||||
const res = await axios.get(customTranslationsURL, { signal: options?.signal });
|
const res = await axios.get(customTranslationsURL);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,14 @@ import type {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const automationsPath = `${apiEntryUrl}/automations`;
|
const automationsPath = `${apiEntryUrl}/automations`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to get the automations settings
|
* HTTP request to get the automations settings
|
||||||
*/
|
*/
|
||||||
export async function getAutomationSettings(options?: RequestOptions): Promise<AutomationSettings> {
|
export async function getAutomationSettings(): Promise<AutomationSettings> {
|
||||||
const res = await axios.get(automationsPath, { signal: options?.signal });
|
const res = await axios.get(automationsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
|
|
||||||
axios.defaults.validateStatus = (status) => {
|
axios.defaults.validateStatus = (status) => {
|
||||||
return status >= 200 && status < 300;
|
return (status >= 200 && status < 300) || status === 304;
|
||||||
};
|
};
|
||||||
axios.defaults.timeout = axiosConfig.shortTimeout;
|
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import axios from 'axios';
|
|||||||
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests list of known custom fields
|
* Requests list of known custom fields
|
||||||
*/
|
*/
|
||||||
export async function getCustomFields(options?: RequestOptions): Promise<CustomFields> {
|
export async function getCustomFields(): Promise<CustomFields> {
|
||||||
const res = await axios.get(customFieldsPath, { signal: options?.signal });
|
const res = await axios.get(customFieldsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import axios, { AxiosResponse } from 'axios';
|
|||||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
import { createBlob, downloadBlob } from './utils';
|
import { createBlob, downloadBlob } from './utils';
|
||||||
|
|
||||||
const dbPath = `${apiEntryUrl}/db`;
|
const dbPath = `${apiEntryUrl}/db`;
|
||||||
@@ -12,7 +10,7 @@ const dbPath = `${apiEntryUrl}/db`;
|
|||||||
* HTTP request to the current DB
|
* HTTP request to the current DB
|
||||||
*/
|
*/
|
||||||
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||||
return axios.post(`${dbPath}/download`, { filename }, { timeout: axiosConfig.longTimeout });
|
return axios.post(`${dbPath}/download`, { filename });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +37,6 @@ export async function uploadProjectFile(file: File): Promise<MessageResponse> {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('project', file);
|
formData.append('project', file);
|
||||||
const response = await axios.post(`${dbPath}/upload`, formData, {
|
const response = await axios.post(`${dbPath}/upload`, formData, {
|
||||||
timeout: axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -79,11 +76,8 @@ export async function quickProject(data: QuickStartData): Promise<MessageRespons
|
|||||||
/**
|
/**
|
||||||
* HTTP request to get the list of available project files
|
* HTTP request to get the list of available project files
|
||||||
*/
|
*/
|
||||||
export async function getProjects(options?: RequestOptions): Promise<ProjectFileListResponse> {
|
export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||||
const res = await axios.get(`${dbPath}/all`, {
|
const res = await axios.get(`${dbPath}/all`);
|
||||||
signal: options?.signal,
|
|
||||||
timeout: options?.timeout,
|
|
||||||
});
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
|||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
import { downloadBlob } from './utils';
|
import { downloadBlob } from './utils';
|
||||||
|
|
||||||
const excelPath = `${apiEntryUrl}/excel`;
|
const excelPath = `${apiEntryUrl}/excel`;
|
||||||
@@ -13,12 +11,10 @@ const excelPath = `${apiEntryUrl}/excel`;
|
|||||||
* 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
|
||||||
*/
|
*/
|
||||||
export async function upload(file: File, requestOptions?: RequestOptions): Promise<string[]> {
|
export async function upload(file: File): Promise<string[]> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('excel', file);
|
formData.append('excel', file);
|
||||||
const response = await axios.post(`${excelPath}/upload`, formData, {
|
const response = await axios.post(`${excelPath}/upload`, formData, {
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -31,31 +27,19 @@ type PreviewSpreadsheetResponse = {
|
|||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
summary: RundownSummary;
|
summary: RundownSummary;
|
||||||
};
|
};
|
||||||
export async function importRundownPreview(
|
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||||
options: ImportMap,
|
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||||
requestOptions?: RequestOptions,
|
options,
|
||||||
): Promise<PreviewSpreadsheetResponse> {
|
});
|
||||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
|
|
||||||
`${excelPath}/preview`,
|
|
||||||
{
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a xlsx representation of the rundown from the server
|
* Downloads a xlsx representation of the rundown from the server
|
||||||
*/
|
*/
|
||||||
export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) {
|
export async function downloadAsExcel(rundownId: string, fileName?: string) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${excelPath}/${rundownId}/export`, {
|
const response = await axios.get(`${excelPath}/${rundownId}/export`, {
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import axios from 'axios';
|
|||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
import { apiRepoLatest } from '../../externals';
|
||||||
|
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
export type HasUpdate = {
|
export type HasUpdate = {
|
||||||
url: string;
|
url: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -12,8 +10,8 @@ export type HasUpdate = {
|
|||||||
/**
|
/**
|
||||||
* HTTP request to get the latest version and url from github
|
* HTTP request to get the latest version and url from github
|
||||||
*/
|
*/
|
||||||
export async function getLatestVersion(options?: RequestOptions): Promise<HasUpdate> {
|
export async function getLatestVersion(): Promise<HasUpdate> {
|
||||||
const res = await axios.get(apiRepoLatest, { signal: options?.signal });
|
const res = await axios.get(apiRepoLatest);
|
||||||
return {
|
return {
|
||||||
url: res.data.html_url as string,
|
url: res.data.html_url as string,
|
||||||
version: res.data.tag_name as string,
|
version: res.data.tag_name as string,
|
||||||
|
|||||||
@@ -2,19 +2,14 @@ import axios, { AxiosResponse } from 'axios';
|
|||||||
import { ProjectData, ProjectLogoResponse } from 'ontime-types';
|
import { ProjectData, ProjectLogoResponse } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
|
|
||||||
const projectPath = `${apiEntryUrl}/project`;
|
const projectPath = `${apiEntryUrl}/project`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch project data
|
* HTTP request to fetch project data
|
||||||
*/
|
*/
|
||||||
export async function getProjectData(options?: RequestOptions): Promise<ProjectData> {
|
export async function getProjectData(): Promise<ProjectData> {
|
||||||
const res = await axios.get(projectPath, {
|
const res = await axios.get(projectPath);
|
||||||
signal: options?.signal,
|
|
||||||
timeout: options?.timeout,
|
|
||||||
});
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +27,6 @@ export async function uploadProjectLogo(file: File): Promise<AxiosResponse<Proje
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', file);
|
formData.append('image', file);
|
||||||
const response = await axios.post(`${projectPath}/upload`, formData, {
|
const response = await axios.post(`${projectPath}/upload`, formData, {
|
||||||
timeout: axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import { OntimeReport } from 'ontime-types';
|
|||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
|
||||||
import { apiEntryUrl, REPORT } from './constants';
|
import { apiEntryUrl, REPORT } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
export const reportUrl = `${apiEntryUrl}/report`;
|
export const reportUrl = `${apiEntryUrl}/report`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch all reports
|
* HTTP request to fetch all reports
|
||||||
*/
|
*/
|
||||||
export async function fetchReport(options?: RequestOptions): Promise<OntimeReport> {
|
export async function fetchReport(): Promise<OntimeReport> {
|
||||||
const res = await axios.get(reportUrl, { signal: options?.signal });
|
const res = await axios.get(reportUrl);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export type RequestOptions = {
|
|
||||||
signal?: AbortSignal;
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* Keep a short global timeout for regular API requests, but allow
|
|
||||||
* longer windows for file transfer and heavy import/export operations.
|
|
||||||
*/
|
|
||||||
export const axiosConfig = {
|
|
||||||
shortTimeout: 20 * 1000, // 20 seconds
|
|
||||||
longTimeout: 3 * 60 * 1000, // 3 minutes
|
|
||||||
} as const;
|
|
||||||
@@ -2,7 +2,6 @@ import axios, { AxiosResponse } from 'axios';
|
|||||||
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
|
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
type RundownId = string;
|
type RundownId = string;
|
||||||
const rundownPath = `${apiEntryUrl}/rundowns`;
|
const rundownPath = `${apiEntryUrl}/rundowns`;
|
||||||
@@ -12,16 +11,16 @@ const rundownPath = `${apiEntryUrl}/rundowns`;
|
|||||||
/**
|
/**
|
||||||
* HTTP request to fetch a list of existing rundowns
|
* HTTP request to fetch a list of existing rundowns
|
||||||
*/
|
*/
|
||||||
export async function fetchProjectRundownList(options?: RequestOptions): Promise<ProjectRundownsList> {
|
export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
||||||
const res = await axios.get(rundownPath, { signal: options?.signal });
|
const res = await axios.get(rundownPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch all entries in the currently loaded rundown
|
* HTTP request to fetch all entries in the currently loaded rundown
|
||||||
*/
|
*/
|
||||||
export async function fetchCurrentRundown(options?: RequestOptions): Promise<Rundown> {
|
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||||
const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal });
|
const res = await axios.get(`${rundownPath}/current`);
|
||||||
if (!isValidRundown(res.data)) {
|
if (!isValidRundown(res.data)) {
|
||||||
throw new Error('Invalid rundown payload');
|
throw new Error('Invalid rundown payload');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import axios from 'axios';
|
|||||||
import { GetInfo, LinkOptions } from 'ontime-types';
|
import { GetInfo, LinkOptions } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const sessionPath = `${apiEntryUrl}/session`;
|
const sessionPath = `${apiEntryUrl}/session`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve application info
|
* HTTP request to retrieve application info
|
||||||
*/
|
*/
|
||||||
export async function getInfo(options?: RequestOptions): Promise<GetInfo> {
|
export async function getInfo(): Promise<GetInfo> {
|
||||||
const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal });
|
const res = await axios.get(`${sessionPath}/info`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import axios, { AxiosResponse } from 'axios';
|
|||||||
import { Settings } from 'ontime-types';
|
import { Settings } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const settingsPath = `${apiEntryUrl}/settings`;
|
const settingsPath = `${apiEntryUrl}/settings`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve application settings
|
* HTTP request to retrieve application settings
|
||||||
*/
|
*/
|
||||||
export async function getSettings(options?: RequestOptions): Promise<Settings> {
|
export async function getSettings(): Promise<Settings> {
|
||||||
const res = await axios.get(settingsPath, { signal: options?.signal });
|
const res = await axios.get(settingsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ont
|
|||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
|
|
||||||
const sheetsPath = `${apiEntryUrl}/sheets`;
|
const sheetsPath = `${apiEntryUrl}/sheets`;
|
||||||
|
|
||||||
@@ -25,7 +23,6 @@ export const verifyAuthenticationStatus = async (): Promise<{
|
|||||||
export const requestConnection = async (
|
export const requestConnection = async (
|
||||||
file: File,
|
file: File,
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
requestOptions?: RequestOptions,
|
|
||||||
): Promise<{
|
): Promise<{
|
||||||
verification_url: string;
|
verification_url: string;
|
||||||
user_code: string;
|
user_code: string;
|
||||||
@@ -34,8 +31,6 @@ export const requestConnection = async (
|
|||||||
formData.append('client_secret', file);
|
formData.append('client_secret', file);
|
||||||
|
|
||||||
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -58,50 +53,24 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
|
|||||||
export const previewRundown = async (
|
export const previewRundown = async (
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ImportMap,
|
options: ImportMap,
|
||||||
requestOptions?: RequestOptions,
|
|
||||||
): Promise<{
|
): Promise<{
|
||||||
rundown: Rundown;
|
rundown: Rundown;
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
summary: RundownSummary;
|
summary: RundownSummary;
|
||||||
}> => {
|
}> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||||
`${sheetsPath}/${sheetId}/read`,
|
|
||||||
{ options },
|
|
||||||
{
|
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise<string[]> => {
|
export const getWorksheetNames = async (sheetId: string): Promise<string[]> => {
|
||||||
const response: AxiosResponse<string[]> = await axios.post(
|
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`);
|
||||||
`${sheetsPath}/${sheetId}/worksheets`,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to upload the rundown to a google sheet
|
* HTTP request to upload the rundown to a google sheet
|
||||||
*/
|
*/
|
||||||
export const uploadRundown = async (
|
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
||||||
sheetId: string,
|
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options });
|
||||||
options: ImportMap,
|
|
||||||
requestOptions?: RequestOptions,
|
|
||||||
): Promise<void> => {
|
|
||||||
const response = await axios.post(
|
|
||||||
`${sheetsPath}/${sheetId}/write`,
|
|
||||||
{ options },
|
|
||||||
{
|
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import axios from 'axios';
|
|||||||
import { URLPreset } from 'ontime-types';
|
import { URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve all presets
|
* HTTP request to retrieve all presets
|
||||||
*/
|
*/
|
||||||
export async function getUrlPresets(options?: RequestOptions): Promise<URLPreset[]> {
|
export async function getUrlPresets(): Promise<URLPreset[]> {
|
||||||
const res = await axios.get(urlPresetsPath, { signal: options?.signal });
|
const res = await axios.get(urlPresetsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import axios from 'axios';
|
|||||||
import type { ViewSettings } from 'ontime-types';
|
import type { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||||
|
|
||||||
@@ -10,8 +9,8 @@ const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
|||||||
* HTTP request to fetch view settings
|
* HTTP request to fetch view settings
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function getViewSettings(options?: RequestOptions): Promise<ViewSettings> {
|
export async function getViewSettings(): Promise<ViewSettings> {
|
||||||
const res = await axios.get(viewSettingsPath, { signal: options?.signal });
|
const res = await axios.get(viewSettingsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
.title-card__title {
|
.title-card__title {
|
||||||
color: var(--color-override, $viewer-color);
|
color: var(--color-override, $viewer-color);
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
min-height: 1.2em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-card__placeholder {
|
.title-card__placeholder {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import IconButton from '../buttons/IconButton';
|
|||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
|
|
||||||
import { ViewOption } from './viewParams.types';
|
import { ViewOption } from './viewParams.types';
|
||||||
import { getPreservedSearchParams, getURLSearchParamsFromObj } from './viewParams.utils';
|
import { getURLSearchParamsFromObj } from './viewParams.utils';
|
||||||
import { useViewParamsEditorStore } from './viewParamsEditor.store';
|
import { useViewParamsEditorStore } from './viewParamsEditor.store';
|
||||||
import { ViewParamsPresets } from './ViewParamsPresets';
|
import { ViewParamsPresets } from './ViewParamsPresets';
|
||||||
import ViewParamsSection from './ViewParamsSection';
|
import ViewParamsSection from './ViewParamsSection';
|
||||||
@@ -25,19 +25,17 @@ interface EditFormDrawerProps {
|
|||||||
|
|
||||||
export default memo(ViewParamsEditor);
|
export default memo(ViewParamsEditor);
|
||||||
function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [_, setSearchParams] = useSearchParams();
|
||||||
const { data: viewSettings } = useViewSettings();
|
const { data: viewSettings } = useViewSettings();
|
||||||
const { isOpen, close } = useViewParamsEditorStore();
|
const { isOpen, close } = useViewParamsEditorStore();
|
||||||
const isSmallScreen = useIsSmallScreen();
|
const isSmallScreen = useIsSmallScreen();
|
||||||
|
|
||||||
const getPreservedParams = () => getPreservedSearchParams(searchParams, viewOptions);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetParams = () => {
|
const resetParams = () => {
|
||||||
setSearchParams(getPreservedParams());
|
setSearchParams();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||||
@@ -45,10 +43,6 @@ function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
|||||||
|
|
||||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, viewOptions);
|
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, viewOptions);
|
||||||
const preservedParams = getPreservedParams();
|
|
||||||
preservedParams.forEach((value, key) => {
|
|
||||||
newSearchParams.append(key, value);
|
|
||||||
});
|
|
||||||
setSearchParams(newSearchParams);
|
setSearchParams(newSearchParams);
|
||||||
|
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
|
|||||||
@@ -191,26 +191,3 @@ export function getURLSearchParamsFromObj(paramsObj: ViewParamsObj, paramFields:
|
|||||||
|
|
||||||
return newSearchParams;
|
return newSearchParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts search params that are not managed by the view params editor
|
|
||||||
* @param currentParams - The current URL search params
|
|
||||||
* @param paramFields - The view options that define the managed parameters
|
|
||||||
* @returns A new URLSearchParams object with preserved params
|
|
||||||
*/
|
|
||||||
export function getPreservedSearchParams(currentParams: URLSearchParams, paramFields: ViewOption[]) {
|
|
||||||
const managedParamIds = new Set<string>();
|
|
||||||
paramFields.forEach((section) => {
|
|
||||||
section.options.forEach((option) => {
|
|
||||||
managedParamIds.add(option.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const preserved = new URLSearchParams();
|
|
||||||
currentParams.forEach((value, key) => {
|
|
||||||
if (!managedParamIds.has(key)) {
|
|
||||||
preserved.append(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return preserved;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function useAppVersion() {
|
|||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: APP_VERSION,
|
queryKey: APP_VERSION,
|
||||||
queryFn: ({ signal }) => getLatestVersion({ signal }),
|
queryFn: getLatestVersion,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import { automationPlaceholderSettings } from '../models/AutomationSettings';
|
|||||||
export default function useAutomationSettings() {
|
export default function useAutomationSettings() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: AUTOMATION,
|
queryKey: AUTOMATION,
|
||||||
queryFn: ({ signal }) => getAutomationSettings({ signal }),
|
queryFn: getAutomationSettings,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
|
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ const placeholder: CustomFields = {};
|
|||||||
export default function useCustomFields() {
|
export default function useCustomFields() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: CUSTOM_FIELDS,
|
queryKey: CUSTOM_FIELDS,
|
||||||
queryFn: ({ signal }) => getCustomFields({ signal }),
|
queryFn: getCustomFields,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? placeholder, status, isFetching, isError, refetch };
|
return { data: data ?? placeholder, status, isFetching, isError, refetch };
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|||||||
export function useCustomTranslation() {
|
export function useCustomTranslation() {
|
||||||
const { data, status, refetch } = useQuery({
|
const { data, status, refetch } = useQuery({
|
||||||
queryKey: TRANSLATION,
|
queryKey: TRANSLATION,
|
||||||
queryFn: ({ signal }) => getUserTranslation({ signal }),
|
queryFn: getUserTranslation,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ import { ontimePlaceholderInfo } from '../models/Info';
|
|||||||
export default function useInfo() {
|
export default function useInfo() {
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
|
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
|
||||||
queryKey: APP_INFO,
|
queryKey: APP_INFO,
|
||||||
queryFn: ({ signal }) => getInfo({ signal }),
|
queryFn: getInfo,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching };
|
return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching };
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { projectDataPlaceholder } from '../models/ProjectData';
|
|||||||
export default function useProjectData() {
|
export default function useProjectData() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: PROJECT_DATA,
|
queryKey: PROJECT_DATA,
|
||||||
queryFn: ({ signal }) => getProjectData({ signal }),
|
queryFn: getProjectData,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ const placeholderProjectList: ProjectFileListResponse = {
|
|||||||
function useProjectList() {
|
function useProjectList() {
|
||||||
const { data, status, refetch } = useQuery({
|
const { data, status, refetch } = useQuery({
|
||||||
queryKey: PROJECT_LIST,
|
queryKey: PROJECT_LIST,
|
||||||
queryFn: ({ signal }) => getProjects({ signal }),
|
queryFn: getProjects,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
return { data: data ?? placeholderProjectList, status, refetch };
|
return { data: data ?? placeholderProjectList, status, refetch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList
|
|||||||
export function useProjectRundowns() {
|
export function useProjectRundowns() {
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
|
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
|
||||||
queryKey: PROJECT_RUNDOWNS,
|
queryKey: PROJECT_RUNDOWNS,
|
||||||
queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
|
queryFn: fetchProjectRundownList,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import { fetchReport } from '../api/report';
|
|||||||
export default function useReport() {
|
export default function useReport() {
|
||||||
const { data, refetch } = useQuery<OntimeReport>({
|
const { data, refetch } = useQuery<OntimeReport>({
|
||||||
queryKey: REPORT,
|
queryKey: REPORT,
|
||||||
queryFn: ({ signal }) => fetchReport({ signal }),
|
queryFn: fetchReport,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
|
networkMode: 'always',
|
||||||
staleTime: MILLIS_PER_HOUR,
|
staleTime: MILLIS_PER_HOUR,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const cachedRundownPlaceholder: Rundown = {
|
|||||||
export default function useRundown() {
|
export default function useRundown() {
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||||
queryKey: RUNDOWN,
|
queryKey: RUNDOWN,
|
||||||
queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
|
queryFn: fetchCurrentRundown,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
|||||||
export default function useSettings() {
|
export default function useSettings() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: APP_SETTINGS,
|
queryKey: APP_SETTINGS,
|
||||||
queryFn: ({ signal }) => getSettings({ signal }),
|
queryFn: getSettings,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const unobfuscated = { ...data };
|
const unobfuscated = { ...data };
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface FetchProps {
|
|||||||
export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
||||||
const { data, status, isError, refetch } = useQuery({
|
const { data, status, isError, refetch } = useQuery({
|
||||||
queryKey: URL_PRESETS,
|
queryKey: URL_PRESETS,
|
||||||
queryFn: ({ signal }) => getUrlPresets({ signal }),
|
queryFn: getUrlPresets,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
enabled: !skip,
|
enabled: !skip,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
|||||||
export default function useViewSettings() {
|
export default function useViewSettings() {
|
||||||
const { data, status } = useQuery({
|
const { data, status } = useQuery({
|
||||||
queryKey: VIEW_SETTINGS,
|
queryKey: VIEW_SETTINGS,
|
||||||
queryFn: ({ signal }) => getViewSettings({ signal }),
|
queryFn: getViewSettings,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
staleTime: MILLIS_PER_HOUR,
|
staleTime: MILLIS_PER_HOUR,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,31 +6,15 @@ import {
|
|||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isOntimeGroup,
|
isOntimeGroup,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
OntimeDelay,
|
|
||||||
OntimeEntry,
|
OntimeEntry,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeGroup,
|
|
||||||
OntimeMilestone,
|
|
||||||
PatchWithId,
|
|
||||||
Rundown,
|
Rundown,
|
||||||
SupportedEntry,
|
SupportedEntry,
|
||||||
TimeField,
|
TimeField,
|
||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
|
TransientEventPayload,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, swapEventData } from 'ontime-utils';
|
||||||
addToRundown,
|
|
||||||
createDelay,
|
|
||||||
createEvent,
|
|
||||||
createGroup,
|
|
||||||
createMilestone,
|
|
||||||
dayInMs,
|
|
||||||
generateId,
|
|
||||||
getInsertAfterId,
|
|
||||||
MILLIS_PER_SECOND,
|
|
||||||
parseUserTime,
|
|
||||||
resolveInsertParent,
|
|
||||||
swapEventData,
|
|
||||||
} from 'ontime-utils';
|
|
||||||
|
|
||||||
import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils';
|
import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils';
|
||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
@@ -101,64 +85,9 @@ export const useEntryActions = () => {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: addEntryMutation } = useMutation({
|
const { mutateAsync: addEntryMutation } = useMutation({
|
||||||
mutationFn: ([rundownId, entry]: [string, PatchWithId & InsertOptions]) => postAddEntry(rundownId, entry),
|
mutationFn: ([rundownId, entry]: Parameters<typeof postAddEntry>) => postAddEntry(rundownId, entry),
|
||||||
onMutate: async ([_rundownId, entry]) => {
|
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
|
|
||||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
|
||||||
|
|
||||||
if (previousData) {
|
|
||||||
const optimisticEntry = createOptimisticEntry(entry);
|
|
||||||
const parentId = resolveInsertParent(previousData, entry);
|
|
||||||
const maybeParent = parentId ? previousData.entries[parentId] : null;
|
|
||||||
const parent = maybeParent && isOntimeGroup(maybeParent) ? maybeParent : null;
|
|
||||||
const afterId = getInsertAfterId(previousData, parent, entry.after, entry.before);
|
|
||||||
|
|
||||||
// create a mutable copy — addToRundown mutates in place using immutable array operations
|
|
||||||
const newRundown: Rundown = {
|
|
||||||
...previousData,
|
|
||||||
entries: { ...previousData.entries },
|
|
||||||
revision: -1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// copy the parent group so we don't mutate the original
|
|
||||||
if (parent && parentId) {
|
|
||||||
newRundown.entries[parentId] = { ...parent, entries: [...parent.entries] };
|
|
||||||
}
|
|
||||||
|
|
||||||
addToRundown(
|
|
||||||
newRundown,
|
|
||||||
optimisticEntry,
|
|
||||||
afterId,
|
|
||||||
parent ? (newRundown.entries[parent.id] as OntimeGroup) : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
queryClient.setQueryData<Rundown>(RUNDOWN, newRundown);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { previousData };
|
|
||||||
},
|
|
||||||
onSuccess: (response) => {
|
|
||||||
if (!response.data) return;
|
|
||||||
|
|
||||||
const serverEntry = response.data;
|
|
||||||
const currentData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
|
||||||
|
|
||||||
if (currentData) {
|
|
||||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
|
||||||
...currentData,
|
|
||||||
entries: { ...currentData.entries, [serverEntry.id]: serverEntry },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (_error, _variables, context) => {
|
|
||||||
if (context?.previousData) {
|
|
||||||
queryClient.setQueryData<Rundown>(RUNDOWN, context.previousData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,15 +102,7 @@ export const useEntryActions = () => {
|
|||||||
throw new Error('Rundown not initialised');
|
throw new Error('Rundown not initialised');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEntry: PatchWithId & InsertOptions = { ...entry, id: generateId() };
|
const newEntry: TransientEventPayload = { ...entry, id: generateId() };
|
||||||
|
|
||||||
// handle adding options that concern all event types
|
|
||||||
if (options?.after) {
|
|
||||||
newEntry.after = options.after;
|
|
||||||
}
|
|
||||||
if (options?.before) {
|
|
||||||
newEntry.before = options.before;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ************* CHECK OPTIONS specific to events
|
// ************* CHECK OPTIONS specific to events
|
||||||
if (isOntimeEvent(newEntry)) {
|
if (isOntimeEvent(newEntry)) {
|
||||||
@@ -221,6 +142,14 @@ export const useEntryActions = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle adding options that concern all event type
|
||||||
|
if (options?.after) {
|
||||||
|
(newEntry as TransientEventPayload).after = options.after;
|
||||||
|
}
|
||||||
|
if (options?.before) {
|
||||||
|
(newEntry as TransientEventPayload).before = options.before;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addEntryMutation([rundownId, newEntry]);
|
await addEntryMutation([rundownId, newEntry]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -976,29 +905,3 @@ function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) {
|
|||||||
|
|
||||||
return { entries, order, flatOrder };
|
return { entries, order, flatOrder };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to create an optimistic entry for immediate cache insertion
|
|
||||||
*/
|
|
||||||
function createOptimisticEntry(payload: PatchWithId & InsertOptions): OntimeEntry {
|
|
||||||
const { after: _after, before: _before, ...entryData } = payload;
|
|
||||||
const id = entryData.id;
|
|
||||||
let parent: EntryId | null = null;
|
|
||||||
if ('parent' in entryData && entryData.parent) {
|
|
||||||
parent = entryData.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (entryData.type) {
|
|
||||||
case SupportedEntry.Event: {
|
|
||||||
return createEvent({ ...entryData, id, parent }) as OntimeEvent;
|
|
||||||
}
|
|
||||||
case SupportedEntry.Delay:
|
|
||||||
return createDelay({ id, duration: (entryData as Partial<OntimeDelay>).duration ?? 0, parent });
|
|
||||||
case SupportedEntry.Group:
|
|
||||||
return createGroup({ ...(entryData as Partial<OntimeGroup>), id });
|
|
||||||
case SupportedEntry.Milestone:
|
|
||||||
return createMilestone({ ...(entryData as Partial<OntimeMilestone>), id, parent });
|
|
||||||
default:
|
|
||||||
throw new Error('Unknown entry type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
|
||||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||||
|
|
||||||
import { isOntimeCloud } from '../externals';
|
import { isOntimeCloud } from '../externals';
|
||||||
@@ -9,15 +8,10 @@ export const ontimeQueryClient = new QueryClient({
|
|||||||
queries: {
|
queries: {
|
||||||
gcTime: 10 * MILLIS_PER_MINUTE,
|
gcTime: 10 * MILLIS_PER_MINUTE,
|
||||||
// staleTime: MILLIS_PER_HOUR, //TODO: when all routes have implemented refetch signal from server, we can the assume that the data is not stale until we get the signal
|
// staleTime: MILLIS_PER_HOUR, //TODO: when all routes have implemented refetch signal from server, we can the assume that the data is not stale until we get the signal
|
||||||
networkMode: isOntimeCloud ? 'online' : 'always',
|
networkMode: 'always',
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: (failureCount, error) => {
|
retry: 5,
|
||||||
if (axios.isCancel(error)) {
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return failureCount < 5;
|
|
||||||
},
|
|
||||||
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 5000),
|
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +21,6 @@ export const ontimeQueryClient = new QueryClient({
|
|||||||
* - use 'online' for clients that are connected to the cloud
|
* - use 'online' for clients that are connected to the cloud
|
||||||
*/
|
*/
|
||||||
networkMode: isOntimeCloud ? 'online' : 'always',
|
networkMode: isOntimeCloud ? 'online' : 'always',
|
||||||
retry: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,13 +38,7 @@ import { patchRuntime, patchRuntimeProperty } from '../stores/runtime';
|
|||||||
|
|
||||||
let websocket: WebSocket | null = null;
|
let websocket: WebSocket | null = null;
|
||||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
const socketConfig = {
|
const reconnectInterval = 1000;
|
||||||
reconnectBaseInterval: 1000, // 1 second
|
|
||||||
reconnectMaxInterval: 30000, // 30 seconds
|
|
||||||
reconnectMinInterval: 500, // 0.5 seconds
|
|
||||||
reconnectJitter: 0.25,
|
|
||||||
offlineAttemptsThreshold: 2, // when we consider the client disconnected
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export let hasConnected = false;
|
export let hasConnected = false;
|
||||||
export let reconnectAttempts = 0;
|
export let reconnectAttempts = 0;
|
||||||
@@ -55,11 +49,7 @@ export const connectSocket = () => {
|
|||||||
const preferredClientName = getClientName();
|
const preferredClientName = getClientName();
|
||||||
|
|
||||||
websocket.onopen = () => {
|
websocket.onopen = () => {
|
||||||
const isReconnect = hasConnected;
|
clearTimeout(reconnectTimeout as NodeJS.Timeout);
|
||||||
if (reconnectTimeout) {
|
|
||||||
clearTimeout(reconnectTimeout);
|
|
||||||
reconnectTimeout = null;
|
|
||||||
}
|
|
||||||
hasConnected = true;
|
hasConnected = true;
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
|
|
||||||
@@ -69,38 +59,24 @@ export const connectSocket = () => {
|
|||||||
path: window.location.pathname + window.location.search,
|
path: window.location.pathname + window.location.search,
|
||||||
name: preferredClientName,
|
name: preferredClientName,
|
||||||
});
|
});
|
||||||
|
invalidateAllCaches(); // assume all data to be stale after a reconnect
|
||||||
if (isReconnect) {
|
|
||||||
invalidateAllCaches();
|
|
||||||
}
|
|
||||||
setOnlineStatus(true);
|
setOnlineStatus(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
websocket.onclose = () => {
|
websocket.onclose = () => {
|
||||||
console.warn('WebSocket disconnected');
|
console.warn('WebSocket disconnected');
|
||||||
if (reconnectTimeout) {
|
|
||||||
clearTimeout(reconnectTimeout);
|
|
||||||
reconnectTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exponentialDelay = Math.min(
|
|
||||||
socketConfig.reconnectBaseInterval * 2 ** reconnectAttempts,
|
|
||||||
socketConfig.reconnectMaxInterval,
|
|
||||||
);
|
|
||||||
const jitterOffset = exponentialDelay * socketConfig.reconnectJitter * (Math.random() * 2 - 1);
|
|
||||||
const delay = Math.max(socketConfig.reconnectMinInterval, Math.round(exponentialDelay + jitterOffset));
|
|
||||||
|
|
||||||
|
// we decide to allows reconnect
|
||||||
reconnectTimeout = setTimeout(() => {
|
reconnectTimeout = setTimeout(() => {
|
||||||
reconnectTimeout = null;
|
if (reconnectAttempts > 2) {
|
||||||
if (reconnectAttempts > socketConfig.offlineAttemptsThreshold) {
|
|
||||||
setOnlineStatus(false);
|
setOnlineStatus(false);
|
||||||
}
|
}
|
||||||
console.warn(`WebSocket: reconnecting now (#${reconnectAttempts + 1}, waited ${delay}ms)`);
|
console.warn('WebSocket: attempting reconnect');
|
||||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||||
reconnectAttempts += 1;
|
reconnectAttempts += 1;
|
||||||
connectSocket();
|
connectSocket();
|
||||||
}
|
}
|
||||||
}, delay);
|
}, reconnectInterval);
|
||||||
};
|
};
|
||||||
|
|
||||||
websocket.onerror = (error) => {
|
websocket.onerror = (error) => {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default function GeneralSettings() {
|
|||||||
} = useForm<Settings>({
|
} = useForm<Settings>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
|
values: data,
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDirtyValues: true,
|
keepDirtyValues: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default function ViewSettings() {
|
|||||||
formState: { isSubmitting, isDirty, errors },
|
formState: { isSubmitting, isDirty, errors },
|
||||||
} = useForm<ViewSettingsType>({
|
} = useForm<ViewSettingsType>({
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
|
values: data,
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDirtyValues: true,
|
keepDirtyValues: true,
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-4
@@ -1,4 +1,4 @@
|
|||||||
import { lazy, Suspense, useEffect, useRef, useState } from 'react';
|
import { lazy, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../../common/api/assets';
|
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../../common/api/assets';
|
||||||
import Button from '../../../../../common/components/buttons/Button';
|
import Button from '../../../../../common/components/buttons/Button';
|
||||||
@@ -81,9 +81,7 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
|
|||||||
showCloseButton
|
showCloseButton
|
||||||
showBackdrop
|
showBackdrop
|
||||||
bodyElements={
|
bodyElements={
|
||||||
<Suspense fallback={null}>
|
<CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} />
|
||||||
<CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} />
|
|
||||||
</Suspense>
|
|
||||||
}
|
}
|
||||||
footerElements={
|
footerElements={
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
|
|||||||
@@ -90,10 +90,7 @@ const staticOptions = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// a child of navigation or a child of secondary navigation
|
export type SettingsOptionId = (typeof staticOptions)[number]['id'];
|
||||||
export type SettingsOptionId =
|
|
||||||
| (typeof staticOptions)[number]['id']
|
|
||||||
| Extract<(typeof staticOptions)[number], { secondary: object }>['secondary'][number]['id'];
|
|
||||||
|
|
||||||
export function useAppSettingsMenu() {
|
export function useAppSettingsMenu() {
|
||||||
const { data } = useAppVersion();
|
const { data } = useAppVersion();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-phase='overtime'] {
|
&[data-phase='overtime'] {
|
||||||
color: $playback-negative;
|
color: $playback-negative;;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-phase='pending'] {
|
&[data-phase='pending'] {
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { type HTMLProps, forwardRef, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { type HTMLProps, forwardRef, Fragment, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { TbFlagFilled } from 'react-icons/tb';
|
import { TbFlagFilled } from 'react-icons/tb';
|
||||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
import { closestCenter, DndContext } from '@dnd-kit/core';
|
import { closestCenter, DndContext } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import {
|
import { type EntryId, type Rundown, isOntimeEvent, isOntimeGroup, Playback, SupportedEntry } from 'ontime-types';
|
||||||
type EntryId,
|
|
||||||
type Rundown as RundownType,
|
|
||||||
isOntimeEvent,
|
|
||||||
isOntimeGroup,
|
|
||||||
Playback,
|
|
||||||
SupportedEntry,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { useEntryActionsContext } from '../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../common/context/EntryActionsContext';
|
||||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||||
@@ -26,18 +19,19 @@ import RundownGroup from './rundown-group/RundownGroup';
|
|||||||
import RundownGroupEnd from './rundown-group/RundownGroupEnd';
|
import RundownGroupEnd from './rundown-group/RundownGroupEnd';
|
||||||
import { filterVisibleEntries, makeSortableList } from './rundown.utils';
|
import { filterVisibleEntries, makeSortableList } from './rundown.utils';
|
||||||
import RundownEmpty from './RundownEmpty';
|
import RundownEmpty from './RundownEmpty';
|
||||||
import RundownEntry from './RundownEntry';
|
|
||||||
import { useCollapsedGroups } from './useCollapsedGroups';
|
import { useCollapsedGroups } from './useCollapsedGroups';
|
||||||
import { useEditorFollowMode } from './useEditorFollowMode';
|
import { useEditorFollowMode } from './useEditorFollowMode';
|
||||||
import { useEventSelection } from './useEventSelection';
|
import { useEventSelection } from './useEventSelection';
|
||||||
|
|
||||||
import style from './Rundown.module.scss';
|
import style from './Rundown.module.scss';
|
||||||
|
|
||||||
|
const RundownEntry = lazy(() => import('./RundownEntry'));
|
||||||
|
|
||||||
interface RundownProps {
|
interface RundownProps {
|
||||||
entries: RundownType['entries'];
|
entries: Rundown['entries'];
|
||||||
id: RundownType['id'];
|
id: Rundown['id'];
|
||||||
order: RundownType['order'];
|
order: Rundown['order'];
|
||||||
flatOrder: RundownType['flatOrder'];
|
flatOrder: Rundown['flatOrder'];
|
||||||
rundownMetadata: RundownMetadataObject;
|
rundownMetadata: RundownMetadataObject;
|
||||||
featureData: {
|
featureData: {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
@@ -151,27 +145,19 @@ export default function Rundown({ order, flatOrder, entries, id, rundownMetadata
|
|||||||
};
|
};
|
||||||
}, [visibleData, dnd.isDraggingRef, setScrollHandler]);
|
}, [visibleData, dnd.isDraggingRef, setScrollHandler]);
|
||||||
|
|
||||||
// Keep selected cursor visible by expanding its parent group in any mode.
|
|
||||||
// Finder can select entries while layout is in Run mode, where we do not follow cursor scroll.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!cursor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = entries[cursor];
|
|
||||||
if (entry && 'parent' in entry) {
|
|
||||||
expandGroup(entry.parent);
|
|
||||||
}
|
|
||||||
}, [cursor, entries, expandGroup]);
|
|
||||||
|
|
||||||
// Auto-follow behavior in Edit mode: follow the user's cursor
|
// Auto-follow behavior in Edit mode: follow the user's cursor
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dnd.isDraggingRef.current || editorMode !== AppMode.Edit || !cursor) {
|
if (dnd.isDraggingRef.current || editorMode !== AppMode.Edit || !cursor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open parent group if the target is inside a collapsed group
|
||||||
|
const entry = entries[cursor];
|
||||||
|
if (entry && 'parent' in entry) {
|
||||||
|
expandGroup(entry.parent);
|
||||||
|
}
|
||||||
scrollToEntry(cursor);
|
scrollToEntry(cursor);
|
||||||
}, [editorMode, cursor, scrollToEntry, dnd.isDraggingRef]);
|
}, [editorMode, cursor, entries, expandGroup, scrollToEntry, dnd.isDraggingRef]);
|
||||||
|
|
||||||
// Auto-follow behavior in Run mode: follow the currently playing event
|
// Auto-follow behavior in Run mode: follow the currently playing event
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -331,7 +317,7 @@ export default function Rundown({ order, flatOrder, entries, id, rundownMetadata
|
|||||||
|
|
||||||
// Virtuoso components - extracted to prevent recreation on every render
|
// Virtuoso components - extracted to prevent recreation on every render
|
||||||
const VirtuosoListComponent = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(({ children, ...props }, ref) => (
|
const VirtuosoListComponent = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(({ children, ...props }, ref) => (
|
||||||
<div ref={ref} {...props} className={style.list}>
|
<div ref={ref} className={style.list} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rundownRoot {
|
.rundownRoot {
|
||||||
height: calc(100% - 1rem);
|
height: calc(100% - 1.5rem);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
padding-bottom: 0;
|
|
||||||
box-shadow: $box-shadow-right;
|
box-shadow: $box-shadow-right;
|
||||||
|
|
||||||
flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
|
flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ function EventEditorEmpty() {
|
|||||||
<Kbd>{deviceAlt}</Kbd>
|
<Kbd>{deviceAlt}</Kbd>
|
||||||
<AuxKey>+</AuxKey>
|
<AuxKey>+</AuxKey>
|
||||||
<Kbd>Backspace</Kbd>
|
<Kbd>Backspace</Kbd>
|
||||||
|
<AuxKey>/</AuxKey>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className={style.spacer} />
|
<tr className={style.spacer} />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useHotkeys } from '@mantine/hooks';
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { type OntimeEntry, EntryId, SupportedEntry } from 'ontime-types';
|
import { type OntimeEntry, EntryId, SupportedEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
|
||||||
import { useEventSelection } from '../useEventSelection';
|
import { useEventSelection } from '../useEventSelection';
|
||||||
|
|
||||||
interface UseRundownKeyboardOptions {
|
interface UseRundownKeyboardOptions {
|
||||||
@@ -21,20 +20,6 @@ interface UseRundownKeyboardOptions {
|
|||||||
setEntryCopyId: (id: EntryId | null, mode?: 'copy' | 'cut') => void;
|
setEntryCopyId: (id: EntryId | null, mode?: 'copy' | 'cut') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true when a keyboard event target is a text input element.
|
|
||||||
* Use this to avoid intercepting browser-native copy/cut/paste shortcuts
|
|
||||||
* while users are typing in form fields.
|
|
||||||
*/
|
|
||||||
function isEditableElement(target: EventTarget | null): boolean {
|
|
||||||
if (!(target instanceof HTMLElement)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagName = target.tagName.toLowerCase();
|
|
||||||
return tagName === 'input' || tagName === 'textarea';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRundownKeyboard({
|
export function useRundownKeyboard({
|
||||||
cursor,
|
cursor,
|
||||||
commands,
|
commands,
|
||||||
@@ -189,50 +174,10 @@ export function useRundownKeyboard({
|
|||||||
{ preventDefault: true, usePhysicalKeys: true },
|
{ preventDefault: true, usePhysicalKeys: true },
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
['mod + C', () => setEntryCopyId(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
'mod + C',
|
['mod + X', () => setEntryCopyId(cursor, 'cut'), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
(event) => {
|
['mod + V', () => commands.insertCopyAtId(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
if (cursor === null || isEditableElement(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
setEntryCopyId(cursor);
|
|
||||||
},
|
|
||||||
{ usePhysicalKeys: true },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'mod + X',
|
|
||||||
(event) => {
|
|
||||||
if (cursor === null || isEditableElement(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
setEntryCopyId(cursor, 'cut');
|
|
||||||
},
|
|
||||||
{ usePhysicalKeys: true },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'mod + V',
|
|
||||||
(event) => {
|
|
||||||
if (isEditableElement(event.target) || useEntryCopy.getState().entryCopyId === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
commands.insertCopyAtId(cursor);
|
|
||||||
},
|
|
||||||
{ usePhysicalKeys: true },
|
|
||||||
],
|
|
||||||
['mod + D', () => commands.cloneEntry(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
['mod + D', () => commands.cloneEntry(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
[
|
['mod + shift + V', () => commands.insertCopyAtId(cursor, true), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
'mod + shift + V',
|
|
||||||
(event) => {
|
|
||||||
if (isEditableElement(event.target) || useEntryCopy.getState().entryCopyId === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
commands.insertCopyAtId(cursor, true);
|
|
||||||
},
|
|
||||||
{ usePhysicalKeys: true },
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
padding-inline: 1rem 2rem;
|
padding-inline: 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
:is([data-target='small-device']) & {
|
:is([data-target='small-device']) & {
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
@@ -72,3 +72,10 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,49 +76,43 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp
|
|||||||
<Toolbar.Root className={style.header}>
|
<Toolbar.Root className={style.header}>
|
||||||
{showRunEditToggle && (
|
{showRunEditToggle && (
|
||||||
<ToggleGroup value={[editorMode]} onValueChange={toggleAppMode} className={style.group}>
|
<ToggleGroup value={[editorMode]} onValueChange={toggleAppMode} className={style.group}>
|
||||||
<Tooltip
|
<Tooltip text='Live playback view with auto-follow' render={<span />}>
|
||||||
text='Live playback view with auto-follow'
|
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
|
||||||
render={<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton} />}
|
Run
|
||||||
>
|
</Toolbar.Button>
|
||||||
Run
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip text='Manual editing without playback automation' render={<span />}>
|
||||||
text='Manual editing without playback automation'
|
<Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.radioButton}>
|
||||||
render={<Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.radioButton} />}
|
Edit
|
||||||
>
|
</Toolbar.Button>
|
||||||
Edit
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ToggleGroup value={[viewMode]} onValueChange={toggleViewMode} className={style.group}>
|
<ToggleGroup value={[viewMode]} onValueChange={toggleViewMode} className={style.group}>
|
||||||
<Tooltip
|
<Tooltip text='View rundown in list mode' render={<span />}>
|
||||||
text='View rundown in list mode'
|
<Toolbar.Button render={<Toggle />} value={RundownViewMode.List} className={style.radioButton}>
|
||||||
render={<Toolbar.Button render={<Toggle />} value={RundownViewMode.List} className={style.radioButton} />}
|
List
|
||||||
>
|
</Toolbar.Button>
|
||||||
List
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip text='View rundown in table mode' render={<span />}>
|
||||||
text='View rundown in table mode'
|
<Toolbar.Button render={<Toggle />} value={RundownViewMode.Table} className={style.radioButton}>
|
||||||
render={<Toolbar.Button render={<Toggle />} value={RundownViewMode.Table} className={style.radioButton} />}
|
Table
|
||||||
>
|
</Toolbar.Button>
|
||||||
Table
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
|
|
||||||
{showOffsetToggle && (
|
{showOffsetToggle && (
|
||||||
<ToggleGroup value={[offsetMode]} onValueChange={toggleOffsetMode} className={style.group}>
|
<ToggleGroup value={[offsetMode]} onValueChange={toggleOffsetMode} className={style.group}>
|
||||||
<Tooltip
|
<Tooltip text='Offsets use fixed clock time' render={<span />}>
|
||||||
text='Offsets use fixed clock time'
|
<Toolbar.Button render={<Toggle />} value={OffsetMode.Absolute} className={style.radioButton}>
|
||||||
render={<Toolbar.Button render={<Toggle />} value={OffsetMode.Absolute} className={style.radioButton} />}
|
Absolute
|
||||||
>
|
</Toolbar.Button>
|
||||||
Absolute
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip text='Offsets follow the rundown relative start' render={<span />}>
|
||||||
text='Offsets follow the rundown relative start'
|
<Toolbar.Button render={<Toggle />} value={OffsetMode.Relative} className={style.radioButton}>
|
||||||
render={<Toolbar.Button render={<Toggle />} value={OffsetMode.Relative} className={style.radioButton} />}
|
Relative
|
||||||
>
|
</Toolbar.Button>
|
||||||
Relative
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function RundownMenu({ allowNavigation }: RundownMenuProps) {
|
|||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Manage Rundowns...',
|
label: 'Manage Rundowns...',
|
||||||
icon: IoList,
|
icon: IoList,
|
||||||
onClick: () => setLocation('manage__rundowns'),
|
onClick: () => setLocation('manage'),
|
||||||
disabled: !allowNavigation,
|
disabled: !allowNavigation,
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ export function useSelectAndRevealEntry(rundownId: string) {
|
|||||||
({ id, index, parent }: SelectAndRevealOptions) => {
|
({ id, index, parent }: SelectAndRevealOptions) => {
|
||||||
expandGroup(parent);
|
expandGroup(parent);
|
||||||
selectEntry({ id, index, selectMode: 'click' });
|
selectEntry({ id, index, selectMode: 'click' });
|
||||||
// Wait one frame so collapsed-group expansion is reflected in rendered/visible entries before scrolling.
|
scrollToEntry(id);
|
||||||
requestAnimationFrame(() => {
|
|
||||||
scrollToEntry(id);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[expandGroup, scrollToEntry, selectEntry],
|
[expandGroup, scrollToEntry, selectEntry],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const langDe: TranslationObject = {
|
|||||||
'countdown.to_start': 'Zeit bis zum Start',
|
'countdown.to_start': 'Zeit bis zum Start',
|
||||||
'countdown.waiting': 'Warten auf den Veranstaltungsbeginn',
|
'countdown.waiting': 'Warten auf den Veranstaltungsbeginn',
|
||||||
'countdown.overtime': 'überfällig',
|
'countdown.overtime': 'überfällig',
|
||||||
'countdown.all_have_finished': 'Alle ausgewählten Veranstaltungen sind beendet',
|
|
||||||
'timeline.live': 'live',
|
'timeline.live': 'live',
|
||||||
'timeline.done': 'Beendet',
|
'timeline.done': 'Beendet',
|
||||||
'timeline.due': 'fällig',
|
'timeline.due': 'fällig',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const langEs: TranslationObject = {
|
|||||||
'countdown.to_start': 'Tiempo para comenzar',
|
'countdown.to_start': 'Tiempo para comenzar',
|
||||||
'countdown.waiting': 'Esperando el inicio del evento',
|
'countdown.waiting': 'Esperando el inicio del evento',
|
||||||
'countdown.overtime': 'en tiempo extra',
|
'countdown.overtime': 'en tiempo extra',
|
||||||
'countdown.all_have_finished': 'Todos los eventos seleccionados han terminado',
|
|
||||||
'timeline.live': 'live',
|
'timeline.live': 'live',
|
||||||
'timeline.done': 'Terminado',
|
'timeline.done': 'Terminado',
|
||||||
'timeline.due': 'pendiente',
|
'timeline.due': 'pendiente',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const langFr: TranslationObject = {
|
|||||||
'countdown.to_start': 'Évènement commence dans',
|
'countdown.to_start': 'Évènement commence dans',
|
||||||
'countdown.waiting': 'En attente du début de l’évènement',
|
'countdown.waiting': 'En attente du début de l’évènement',
|
||||||
'countdown.overtime': 'en dépassement',
|
'countdown.overtime': 'en dépassement',
|
||||||
'countdown.all_have_finished': 'Tous les évènements sélectionnés ont terminé',
|
|
||||||
'timeline.live': 'live',
|
'timeline.live': 'live',
|
||||||
'timeline.done': 'Terminé',
|
'timeline.done': 'Terminé',
|
||||||
'timeline.due': 'dû',
|
'timeline.due': 'dû',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const langIt: TranslationObject = {
|
|||||||
'countdown.to_start': 'Tempo alla partenza',
|
'countdown.to_start': 'Tempo alla partenza',
|
||||||
'countdown.waiting': "In attesa dell'inizio dell'evento",
|
'countdown.waiting': "In attesa dell'inizio dell'evento",
|
||||||
'countdown.overtime': 'in ritardo',
|
'countdown.overtime': 'in ritardo',
|
||||||
'countdown.all_have_finished': 'Tutti gli eventi selezionati sono finiti',
|
|
||||||
'timeline.live': 'live',
|
'timeline.live': 'live',
|
||||||
'timeline.done': 'Terminato',
|
'timeline.done': 'Terminato',
|
||||||
'timeline.due': 'previsto',
|
'timeline.due': 'previsto',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const langPt: TranslationObject = {
|
|||||||
'countdown.to_start': 'Tempo para iniciar',
|
'countdown.to_start': 'Tempo para iniciar',
|
||||||
'countdown.waiting': 'Aguardando o início do evento',
|
'countdown.waiting': 'Aguardando o início do evento',
|
||||||
'countdown.overtime': 'em tempo extra',
|
'countdown.overtime': 'em tempo extra',
|
||||||
'countdown.all_have_finished': 'Todos os eventos selecionados terminaram',
|
|
||||||
'timeline.live': 'live',
|
'timeline.live': 'live',
|
||||||
'timeline.done': 'Concluído',
|
'timeline.done': 'Concluído',
|
||||||
'timeline.due': 'Pendente',
|
'timeline.due': 'Pendente',
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function BackstageLoader() {
|
|||||||
|
|
||||||
function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) {
|
function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const { mainSource, secondarySource, extraInfo } = useBackstageOptions();
|
const { secondarySource, extraInfo } = useBackstageOptions();
|
||||||
const { eventNext, eventNow, rundown, selectedEventId, time } = useBackstageSocket();
|
const { eventNext, eventNow, rundown, selectedEventId, time } = useBackstageSocket();
|
||||||
const [blinkClass, setBlinkClass] = useState(false);
|
const [blinkClass, setBlinkClass] = useState(false);
|
||||||
const { height: screenHeight } = useViewportSize();
|
const { height: screenHeight } = useViewportSize();
|
||||||
@@ -64,7 +64,7 @@ function Backstage({ events, customFields, projectData, isMirrored, settings }:
|
|||||||
const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData(
|
const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData(
|
||||||
eventNow,
|
eventNow,
|
||||||
eventNext,
|
eventNext,
|
||||||
mainSource,
|
'title',
|
||||||
secondarySource,
|
secondarySource,
|
||||||
time.playback,
|
time.playback,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const getBackstageOptions = (
|
|||||||
{ value: 'note', label: 'Note' },
|
{ value: 'note', label: 'Note' },
|
||||||
]);
|
]);
|
||||||
const projectDataOptions = makeProjectDataOptions(projectData, [{ value: 'none', label: 'None' }]);
|
const projectDataOptions = makeProjectDataOptions(projectData, [{ value: 'none', label: 'None' }]);
|
||||||
const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
@@ -31,14 +30,6 @@ export const getBackstageOptions = (
|
|||||||
title: OptionTitle.DataSources,
|
title: OptionTitle.DataSources,
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
|
||||||
id: 'main',
|
|
||||||
title: 'Main text',
|
|
||||||
description: 'Select the data source for the main text',
|
|
||||||
type: 'option',
|
|
||||||
values: mainOptions,
|
|
||||||
defaultValue: 'title',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'secondary-src',
|
id: 'secondary-src',
|
||||||
title: 'Event secondary text',
|
title: 'Event secondary text',
|
||||||
@@ -68,7 +59,6 @@ export const getBackstageOptions = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
type BackstageOptions = {
|
type BackstageOptions = {
|
||||||
mainSource: keyof OntimeEvent | null;
|
|
||||||
secondarySource: keyof OntimeEvent | null;
|
secondarySource: keyof OntimeEvent | null;
|
||||||
extraInfo: string | null;
|
extraInfo: string | null;
|
||||||
};
|
};
|
||||||
@@ -82,7 +72,6 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
|
|||||||
const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key);
|
const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mainSource: getValue('main') as keyof OntimeEvent | null,
|
|
||||||
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
|
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
|
||||||
extraInfo: getValue('extra-info'),
|
extraInfo: getValue('extra-info'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { MaybeNumber, MaybeString, OntimeEvent, TimerState, TimerType } from 'ontime-types';
|
||||||
MaybeNumber,
|
|
||||||
MaybeString,
|
|
||||||
OntimeEvent,
|
|
||||||
OntimeGroup,
|
|
||||||
RundownEntries,
|
|
||||||
TimerState,
|
|
||||||
TimerType,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
@@ -74,22 +66,12 @@ export function makeColourString(hex: string | null): string | undefined {
|
|||||||
/**
|
/**
|
||||||
* Retrieves a dynamic property from an event
|
* Retrieves a dynamic property from an event
|
||||||
* Considers custom fields
|
* Considers custom fields
|
||||||
* if rundown entries are provided it can also find the parent title
|
|
||||||
*/
|
*/
|
||||||
export function getPropertyValue(
|
export function getPropertyValue(event: OntimeEvent | null, property: MaybeString): string | undefined {
|
||||||
event: OntimeEvent | null,
|
|
||||||
property: MaybeString,
|
|
||||||
entries?: RundownEntries,
|
|
||||||
): string | undefined {
|
|
||||||
if (!event || typeof property !== 'string' || property === 'none') {
|
if (!event || typeof property !== 'string' || property === 'none') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property === 'parent') {
|
|
||||||
if (!entries || !event.parent) return undefined;
|
|
||||||
return (entries[event.parent] as OntimeGroup)?.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (property.startsWith('custom-')) {
|
if (property.startsWith('custom-')) {
|
||||||
const field = property.split('custom-')[1];
|
const field = property.split('custom-')[1];
|
||||||
return event.custom?.[field];
|
return event.custom?.[field];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView, PlayableEvent } from 'ontime-types';
|
import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../common/components/buttons/Button';
|
import Button from '../../common/components/buttons/Button';
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
@@ -47,9 +47,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
|
|||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
// gather rundown data
|
// gather rundown data
|
||||||
const playableEvents = rundownData.filter((entry): entry is ExtendedEntry<PlayableEvent> => {
|
const playableEvents = rundownData.filter((entry) => isOntimeEvent(entry) && isPlayableEvent(entry));
|
||||||
return isOntimeEvent(entry) && isPlayableEvent(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
// gather presentation data
|
// gather presentation data
|
||||||
const hasEvents = playableEvents.length > 0;
|
const hasEvents = playableEvents.length > 0;
|
||||||
@@ -95,7 +93,6 @@ interface CountdownContentsProps {
|
|||||||
|
|
||||||
function CountdownContents({ playableEvents, subscriptions, goToEditMode }: CountdownContentsProps) {
|
function CountdownContents({ playableEvents, subscriptions, goToEditMode }: CountdownContentsProps) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const { hidePast } = useCountdownOptions();
|
|
||||||
|
|
||||||
if (subscriptions.length === 0) {
|
if (subscriptions.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -109,7 +106,6 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subscribedEvents = getOrderedSubscriptions(subscriptions, playableEvents);
|
const subscribedEvents = getOrderedSubscriptions(subscriptions, playableEvents);
|
||||||
const eventsToShow = !hidePast ? subscribedEvents : subscribedEvents.filter((event) => !event.isPast);
|
|
||||||
|
|
||||||
if (subscribedEvents.length === 0) {
|
if (subscribedEvents.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -122,21 +118,13 @@ function CountdownContents({ playableEvents, subscriptions, goToEditMode }: Coun
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscribedEvents.length === 1 && eventsToShow.length === 1) {
|
if (subscribedEvents.length === 1) {
|
||||||
const event = subscribedEvents.at(0);
|
const event = subscribedEvents.at(0);
|
||||||
if (!event) return null;
|
if (!event) return null;
|
||||||
return <SingleEventCountdown subscribedEvent={event} goToEditMode={goToEditMode} />;
|
return <SingleEventCountdown subscribedEvent={event} goToEditMode={goToEditMode} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventsToShow.length === 0) {
|
return <CountdownSubscriptions subscribedEvents={subscribedEvents} goToEditMode={goToEditMode} />;
|
||||||
return (
|
|
||||||
<div className='empty-container'>
|
|
||||||
<Empty text={getLocalizedString('countdown.all_have_finished')} className='empty-container' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CountdownSubscriptions subscribedEvents={eventsToShow} goToEditMode={goToEditMode} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CountdownClock() {
|
function CountdownClock() {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ interface CountdownSubscriptionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) {
|
export default function CountdownSubscriptions({ subscribedEvents, goToEditMode }: CountdownSubscriptionsProps) {
|
||||||
const { mainSource, secondarySource, showExpected } = useCountdownOptions();
|
const { secondarySource, showExpected } = useCountdownOptions();
|
||||||
const playback = usePlayback();
|
const playback = usePlayback();
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const showFab = useFadeOutOnInactivity(true);
|
const showFab = useFadeOutOnInactivity(true);
|
||||||
@@ -103,7 +103,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
|
|||||||
const isLive = getIsLive(event.id, selectedEventId, playback);
|
const isLive = getIsLive(event.id, selectedEventId, playback);
|
||||||
const isArmed = !isLive && event.id === selectedEventId;
|
const isArmed = !isLive && event.id === selectedEventId;
|
||||||
const countdownEvent = extendEventData(event, currentDay, actualStart, plannedStart, offset, mode, reportData);
|
const countdownEvent = extendEventData(event, currentDay, actualStart, plannedStart, offset, mode, reportData);
|
||||||
const displayTitle = getPropertyValue(event, mainSource ?? 'title');
|
const title = event.title.length ? event.title : ' '; // insert utf-8 empty space to avoid the line collapsing
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@@ -114,7 +114,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
|
|||||||
<div className='sub__binder' style={{ '--user-color': event.colour }} />
|
<div className='sub__binder' style={{ '--user-color': event.colour }} />
|
||||||
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
|
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
|
||||||
<SubscriptionStatus event={countdownEvent} />
|
<SubscriptionStatus event={countdownEvent} />
|
||||||
<div className={cx(['sub__title', !displayTitle && 'subdued'])}>{displayTitle}</div>
|
<div className={cx(['sub__title', !event.title && 'subdued'])}>{title}</div>
|
||||||
{secondaryData && <div className='sub__secondary'>{secondaryData}</div>}
|
{secondaryData && <div className='sub__secondary'>{secondaryData}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface SingleEventCountdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SingleEventCountdown({ subscribedEvent, goToEditMode }: SingleEventCountdownProps) {
|
export default function SingleEventCountdown({ subscribedEvent, goToEditMode }: SingleEventCountdownProps) {
|
||||||
const { mainSource, secondarySource, showExpected } = useCountdownOptions();
|
const { secondarySource, showExpected } = useCountdownOptions();
|
||||||
const showFab = useFadeOutOnInactivity(true);
|
const showFab = useFadeOutOnInactivity(true);
|
||||||
const { data: reportData } = useReport();
|
const { data: reportData } = useReport();
|
||||||
|
|
||||||
@@ -41,8 +41,7 @@ export default function SingleEventCountdown({ subscribedEvent, goToEditMode }:
|
|||||||
|
|
||||||
const { endedAt } = reportData[subscribedEvent.id] ?? { endedAt: null };
|
const { endedAt } = reportData[subscribedEvent.id] ?? { endedAt: null };
|
||||||
const countdownEvent = { ...subscribedEvent, expectedStart, endedAt };
|
const countdownEvent = { ...subscribedEvent, expectedStart, endedAt };
|
||||||
const titleTmp = getPropertyValue(subscribedEvent, mainSource ?? 'title');
|
const title = subscribedEvent.title.length ? subscribedEvent.title : ' '; // insert utf-8 empty space to avoid the line collapsing
|
||||||
const title = titleTmp?.length ? titleTmp : ' '; // insert utf-8 empty space to avoid the line collapsing;
|
|
||||||
const secondaryData = getPropertyValue(subscribedEvent, secondarySource);
|
const secondaryData = getPropertyValue(subscribedEvent, secondarySource);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const getCountdownOptions = (
|
|||||||
customFields: CustomFields,
|
customFields: CustomFields,
|
||||||
persistedSubscriptions: EntryId[],
|
persistedSubscriptions: EntryId[],
|
||||||
): ViewOption[] => {
|
): ViewOption[] => {
|
||||||
const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]);
|
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
{ value: 'none', label: 'None' },
|
{ value: 'none', label: 'None' },
|
||||||
{ value: 'note', label: 'Note' },
|
{ value: 'note', label: 'Note' },
|
||||||
@@ -26,14 +25,6 @@ export const getCountdownOptions = (
|
|||||||
title: OptionTitle.DataSources,
|
title: OptionTitle.DataSources,
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
|
||||||
id: 'main',
|
|
||||||
title: 'Main text',
|
|
||||||
description: 'Select the data source for the main text',
|
|
||||||
type: 'option',
|
|
||||||
values: mainOptions,
|
|
||||||
defaultValue: 'title',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'secondary-src',
|
id: 'secondary-src',
|
||||||
title: 'Event secondary text',
|
title: 'Event secondary text',
|
||||||
@@ -57,19 +48,6 @@ export const getCountdownOptions = (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: OptionTitle.ElementVisibility,
|
|
||||||
collapsible: true,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
id: 'hidePast',
|
|
||||||
title: 'Hide Past Events',
|
|
||||||
description: 'Whether to hide events that have passed',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: OptionTitle.Hidden,
|
title: OptionTitle.Hidden,
|
||||||
options: [
|
options: [
|
||||||
@@ -87,10 +65,8 @@ export const getCountdownOptions = (
|
|||||||
|
|
||||||
type CountdownOptions = {
|
type CountdownOptions = {
|
||||||
subscriptions: EntryId[];
|
subscriptions: EntryId[];
|
||||||
mainSource: keyof OntimeEvent | null;
|
|
||||||
secondarySource: keyof OntimeEvent | null;
|
secondarySource: keyof OntimeEvent | null;
|
||||||
showExpected: boolean;
|
showExpected: boolean;
|
||||||
hidePast: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,10 +87,8 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
subscriptions: getArrayValues('sub'),
|
subscriptions: getArrayValues('sub'),
|
||||||
mainSource: getValue('main') as keyof OntimeEvent | null,
|
|
||||||
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
|
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
|
||||||
showExpected: isStringBoolean(getValue('showExpected')),
|
showExpected: isStringBoolean(getValue('showExpected')),
|
||||||
hidePast: isStringBoolean(getValue('hidePast')),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { ComponentProps, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso';
|
||||||
ContextProp,
|
|
||||||
ItemProps,
|
|
||||||
TableComponents,
|
|
||||||
TableProps,
|
|
||||||
TableVirtuoso,
|
|
||||||
TableVirtuosoHandle,
|
|
||||||
} from 'react-virtuoso';
|
|
||||||
import { useTableNav } from '@table-nav/react';
|
import { useTableNav } from '@table-nav/react';
|
||||||
import { ColumnDef, getCoreRowModel, Table, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { isOntimeDelay, isOntimeGroup, isOntimeMilestone, OntimeEntry, TimeField } from 'ontime-types';
|
import { isOntimeDelay, isOntimeGroup, isOntimeMilestone, OntimeEntry, TimeField } from 'ontime-types';
|
||||||
|
|
||||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||||
@@ -177,28 +170,6 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues
|
|||||||
|
|
||||||
const allLeafColumns = table.getAllLeafColumns();
|
const allLeafColumns = table.getAllLeafColumns();
|
||||||
const { rows } = table.getRowModel();
|
const { rows } = table.getRowModel();
|
||||||
const virtuosoContext = useMemo(
|
|
||||||
() => ({
|
|
||||||
columnSizeVars,
|
|
||||||
cursor,
|
|
||||||
listeners,
|
|
||||||
rows,
|
|
||||||
table,
|
|
||||||
}),
|
|
||||||
[columnSizeVars, cursor, listeners, rows, table],
|
|
||||||
);
|
|
||||||
|
|
||||||
const computeItemKey = useCallback((_: number, item: ExtendedEntry) => item.id, []);
|
|
||||||
const fixedHeaderContent = useCallback(() => {
|
|
||||||
return table.getHeaderGroups().map((headerGroup) => {
|
|
||||||
const HeaderComponent = table.getState().columnSizingInfo.isResizingColumn
|
|
||||||
? CuesheetHeader
|
|
||||||
: SortableCuesheetHeader;
|
|
||||||
|
|
||||||
// if the table is being resized, we render non-sortable headers to avoid performance issues
|
|
||||||
return <HeaderComponent key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />;
|
|
||||||
});
|
|
||||||
}, [cuesheetMode, table]);
|
|
||||||
|
|
||||||
const isLoading = !data || status === 'pending';
|
const isLoading = !data || status === 'pending';
|
||||||
|
|
||||||
@@ -220,146 +191,112 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues
|
|||||||
<TableVirtuoso
|
<TableVirtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={data}
|
data={data}
|
||||||
context={virtuosoContext}
|
|
||||||
style={tableRoot === 'editor' ? { paddingLeft: '1rem' } : undefined}
|
|
||||||
computeItemKey={computeItemKey}
|
|
||||||
increaseViewportBy={{ top: 100, bottom: 200 }}
|
increaseViewportBy={{ top: 100, bottom: 200 }}
|
||||||
components={virtuosoComponents}
|
components={{
|
||||||
fixedHeaderContent={fixedHeaderContent}
|
EmptyPlaceholder: () => <EmptyTableBody text='No data in rundown' />,
|
||||||
|
Table: ({ style: injectedStyles, ...virtuosoProps }) => {
|
||||||
|
return (
|
||||||
|
<table
|
||||||
|
className={style.cuesheet}
|
||||||
|
id='cuesheet'
|
||||||
|
style={{ ...injectedStyles, ...columnSizeVars }}
|
||||||
|
{...listeners}
|
||||||
|
{...virtuosoProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
TableRow: ({ item: _item, style: injectedStyles, ...virtuosoProps }) => {
|
||||||
|
// eslint-disable-next-line react/destructuring-assignment
|
||||||
|
const rowIndex = virtuosoProps['data-index'];
|
||||||
|
const row = rows[rowIndex];
|
||||||
|
const key = row.original.id;
|
||||||
|
const entry = row.original;
|
||||||
|
const hasCursor = entry.id === cursor;
|
||||||
|
|
||||||
|
if (isOntimeGroup(entry)) {
|
||||||
|
return (
|
||||||
|
<GroupRow
|
||||||
|
key={key}
|
||||||
|
groupId={entry.id}
|
||||||
|
colour={entry.colour}
|
||||||
|
rowId={row.id}
|
||||||
|
rowIndex={row.index}
|
||||||
|
table={table}
|
||||||
|
injectedStyles={injectedStyles}
|
||||||
|
hasCursor={hasCursor}
|
||||||
|
{...virtuosoProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOntimeDelay(entry)) {
|
||||||
|
return (
|
||||||
|
<DelayRow
|
||||||
|
key={key}
|
||||||
|
duration={entry.duration}
|
||||||
|
injectedStyles={injectedStyles}
|
||||||
|
hasCursor={hasCursor}
|
||||||
|
{...virtuosoProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOntimeMilestone(entry)) {
|
||||||
|
return (
|
||||||
|
<MilestoneRow
|
||||||
|
key={key}
|
||||||
|
entryId={entry.id}
|
||||||
|
isPast={entry.isPast}
|
||||||
|
parentBgColour={entry.groupColour}
|
||||||
|
parentId={entry.parent}
|
||||||
|
colour={entry.colour}
|
||||||
|
rowId={row.id}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
table={table}
|
||||||
|
injectedStyles={injectedStyles}
|
||||||
|
hasCursor={hasCursor}
|
||||||
|
{...virtuosoProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventRow
|
||||||
|
key={row.id}
|
||||||
|
id={entry.id}
|
||||||
|
eventIndex={entry.eventIndex}
|
||||||
|
colour={entry.colour}
|
||||||
|
isFirstAfterGroup={entry.isFirstAfterGroup}
|
||||||
|
isLoaded={entry.isLoaded}
|
||||||
|
isPast={entry.isPast}
|
||||||
|
groupColour={entry.groupColour}
|
||||||
|
flag={entry.flag}
|
||||||
|
skip={entry.skip}
|
||||||
|
parent={entry.parent}
|
||||||
|
rowId={row.id}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
table={table}
|
||||||
|
injectedStyles={injectedStyles}
|
||||||
|
hasCursor={hasCursor}
|
||||||
|
{...virtuosoProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
TableHead: (virtuosoProps) => <thead className={style.tableHeader} {...virtuosoProps} />,
|
||||||
|
}}
|
||||||
|
fixedHeaderContent={() => {
|
||||||
|
return table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
const HeaderComponent = table.getState().columnSizingInfo.isResizingColumn
|
||||||
|
? CuesheetHeader
|
||||||
|
: SortableCuesheetHeader;
|
||||||
|
|
||||||
|
// if the table is being resized, we render non-sortable headers to avoid performance issues
|
||||||
|
return <HeaderComponent key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />;
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableMenu />
|
<TableMenu />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface CuesheetVirtuosoContext {
|
|
||||||
columnSizeVars: { [key: string]: number };
|
|
||||||
cursor: string | null;
|
|
||||||
listeners: ReturnType<typeof useTableNav>['listeners'];
|
|
||||||
rows: ReturnType<Table<ExtendedEntry>['getRowModel']>['rows'];
|
|
||||||
table: Table<ExtendedEntry>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmptyPlaceholder = memo(function EmptyPlaceholder() {
|
|
||||||
return <EmptyTableBody text='No data in rundown' />;
|
|
||||||
});
|
|
||||||
|
|
||||||
const CuesheetTableElement = memo(function CuesheetTableElement({
|
|
||||||
style: injectedStyles,
|
|
||||||
context,
|
|
||||||
...virtuosoProps
|
|
||||||
}: TableProps & ContextProp<CuesheetVirtuosoContext>) {
|
|
||||||
return (
|
|
||||||
<table
|
|
||||||
className={style.cuesheet}
|
|
||||||
id='cuesheet'
|
|
||||||
style={{ ...injectedStyles, ...context.columnSizeVars }}
|
|
||||||
{...context.listeners}
|
|
||||||
{...virtuosoProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const CuesheetTableHead = memo(function CuesheetTableHead({
|
|
||||||
context: _context,
|
|
||||||
className: _className,
|
|
||||||
...virtuosoProps
|
|
||||||
}: ComponentProps<'thead'> & ContextProp<CuesheetVirtuosoContext>) {
|
|
||||||
return <thead className={style.tableHeader} {...virtuosoProps} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
const CuesheetTableRow = memo(function CuesheetTableRow({
|
|
||||||
item: _item,
|
|
||||||
style: injectedStyles,
|
|
||||||
context,
|
|
||||||
...virtuosoProps
|
|
||||||
}: ItemProps<ExtendedEntry> & ContextProp<CuesheetVirtuosoContext>) {
|
|
||||||
// eslint-disable-next-line react/destructuring-assignment
|
|
||||||
const rowIndex = virtuosoProps['data-index'];
|
|
||||||
const row = context.rows[rowIndex];
|
|
||||||
if (!row) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = row.original.id;
|
|
||||||
const entry = row.original;
|
|
||||||
const hasCursor = entry.id === context.cursor;
|
|
||||||
|
|
||||||
if (isOntimeGroup(entry)) {
|
|
||||||
return (
|
|
||||||
<GroupRow
|
|
||||||
key={key}
|
|
||||||
groupId={entry.id}
|
|
||||||
colour={entry.colour}
|
|
||||||
rowId={row.id}
|
|
||||||
rowIndex={row.index}
|
|
||||||
table={context.table}
|
|
||||||
injectedStyles={injectedStyles}
|
|
||||||
hasCursor={hasCursor}
|
|
||||||
{...virtuosoProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOntimeDelay(entry)) {
|
|
||||||
return (
|
|
||||||
<DelayRow
|
|
||||||
key={key}
|
|
||||||
duration={entry.duration}
|
|
||||||
injectedStyles={injectedStyles}
|
|
||||||
hasCursor={hasCursor}
|
|
||||||
{...virtuosoProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOntimeMilestone(entry)) {
|
|
||||||
return (
|
|
||||||
<MilestoneRow
|
|
||||||
key={key}
|
|
||||||
entryId={entry.id}
|
|
||||||
isPast={entry.isPast}
|
|
||||||
parentBgColour={entry.groupColour}
|
|
||||||
parentId={entry.parent}
|
|
||||||
colour={entry.colour}
|
|
||||||
rowId={row.id}
|
|
||||||
rowIndex={rowIndex}
|
|
||||||
table={context.table}
|
|
||||||
injectedStyles={injectedStyles}
|
|
||||||
hasCursor={hasCursor}
|
|
||||||
{...virtuosoProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EventRow
|
|
||||||
key={row.id}
|
|
||||||
id={entry.id}
|
|
||||||
eventIndex={entry.eventIndex}
|
|
||||||
colour={entry.colour}
|
|
||||||
isFirstAfterGroup={entry.isFirstAfterGroup}
|
|
||||||
isLoaded={entry.isLoaded}
|
|
||||||
isPast={entry.isPast}
|
|
||||||
groupColour={entry.groupColour}
|
|
||||||
flag={entry.flag}
|
|
||||||
skip={entry.skip}
|
|
||||||
parent={entry.parent}
|
|
||||||
rowId={row.id}
|
|
||||||
rowIndex={rowIndex}
|
|
||||||
table={context.table}
|
|
||||||
injectedStyles={injectedStyles}
|
|
||||||
hasCursor={hasCursor}
|
|
||||||
{...virtuosoProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const virtuosoComponents: TableComponents<ExtendedEntry, CuesheetVirtuosoContext> = {
|
|
||||||
EmptyPlaceholder,
|
|
||||||
Table: CuesheetTableElement,
|
|
||||||
TableHead: CuesheetTableHead,
|
|
||||||
TableRow: CuesheetTableRow,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export default function EventRow({
|
|||||||
}}
|
}}
|
||||||
data-cursor={hasCursor}
|
data-cursor={hasCursor}
|
||||||
data-testid='cuesheet-event'
|
data-testid='cuesheet-event'
|
||||||
data-entry-id={id}
|
|
||||||
{...virtuosoProps}
|
{...virtuosoProps}
|
||||||
>
|
>
|
||||||
{cuesheetMode === AppMode.Edit && (
|
{cuesheetMode === AppMode.Edit && (
|
||||||
@@ -128,8 +127,6 @@ export default function EventRow({
|
|||||||
}}
|
}}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
role='cell'
|
role='cell'
|
||||||
data-testid={`cuesheet-cell-${cell.column.id}`}
|
|
||||||
data-column-id={cell.column.id}
|
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+1
-5
@@ -5,14 +5,12 @@ import useReactiveTextInput from '../../../../common/components/input/text-input
|
|||||||
|
|
||||||
interface MultiLineCellProps {
|
interface MultiLineCellProps {
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
fieldId?: string;
|
|
||||||
fieldLabel?: string;
|
|
||||||
handleUpdate: (newValue: string) => void;
|
handleUpdate: (newValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(MultiLineCell);
|
export default memo(MultiLineCell);
|
||||||
|
|
||||||
function MultiLineCell({ initialValue, fieldId, fieldLabel, handleUpdate }: MultiLineCellProps) {
|
function MultiLineCell({ initialValue, handleUpdate }: MultiLineCellProps) {
|
||||||
const ref = useRef<HTMLTextAreaElement | null>(null);
|
const ref = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
@@ -32,8 +30,6 @@ function MultiLineCell({ initialValue, fieldId, fieldLabel, handleUpdate }: Mult
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
data-testid={fieldId ? `cuesheet-editor-${fieldId}` : undefined}
|
|
||||||
aria-label={fieldLabel ? `${fieldLabel} editor` : undefined}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -5,18 +5,13 @@ import useReactiveTextInput from '../../../../common/components/input/text-input
|
|||||||
|
|
||||||
interface SingleLineCellProps {
|
interface SingleLineCellProps {
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
fieldId?: string;
|
|
||||||
fieldLabel?: string;
|
|
||||||
allowSubmitSameValue?: boolean;
|
allowSubmitSameValue?: boolean;
|
||||||
handleUpdate: (newValue: string) => void;
|
handleUpdate: (newValue: string) => void;
|
||||||
handleCancelUpdate?: () => void;
|
handleCancelUpdate?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleLineCell = forwardRef(
|
const SingleLineCell = forwardRef(
|
||||||
(
|
({ initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate }: SingleLineCellProps, inputRef) => {
|
||||||
{ initialValue, fieldId, fieldLabel, allowSubmitSameValue, handleUpdate, handleCancelUpdate }: SingleLineCellProps,
|
|
||||||
inputRef,
|
|
||||||
) => {
|
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
@@ -52,8 +47,6 @@ const SingleLineCell = forwardRef(
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
data-testid={fieldId ? `cuesheet-editor-${fieldId}` : undefined}
|
|
||||||
aria-label={fieldLabel ? `${fieldLabel} editor` : undefined}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-28
@@ -17,10 +17,6 @@ import MutedText from './MutedText';
|
|||||||
import SingleLineCell from './SingleLineCell';
|
import SingleLineCell from './SingleLineCell';
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
|
|
||||||
function getColumnLabel(column: CellContext<ExtendedEntry, unknown>['column']): string {
|
|
||||||
return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MakeStart({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
function MakeStart({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
||||||
if (!table.options.meta) {
|
if (!table.options.meta) {
|
||||||
return null;
|
return null;
|
||||||
@@ -150,14 +146,7 @@ function MakeMultiLineField({ row, column, table }: CellContext<ExtendedEntry, u
|
|||||||
return <GhostedText multiline>{initialValue}</GhostedText>;
|
return <GhostedText multiline>{initialValue}</GhostedText>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <MultiLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||||
<MultiLineCell
|
|
||||||
initialValue={initialValue as string}
|
|
||||||
fieldId={column.id}
|
|
||||||
fieldLabel={getColumnLabel(column)}
|
|
||||||
handleUpdate={update}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function LazyImage({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
function LazyImage({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
||||||
@@ -197,14 +186,7 @@ function MakeSingleLineField({ row, column, table }: CellContext<ExtendedEntry,
|
|||||||
return <GhostedText>{initialValue}</GhostedText>;
|
return <GhostedText>{initialValue}</GhostedText>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <SingleLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||||
<SingleLineCell
|
|
||||||
initialValue={initialValue as string}
|
|
||||||
fieldId={column.id}
|
|
||||||
fieldLabel={getColumnLabel(column)}
|
|
||||||
handleUpdate={update}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeFlagField({ row }: CellContext<ExtendedEntry, unknown>) {
|
function MakeFlagField({ row }: CellContext<ExtendedEntry, unknown>) {
|
||||||
@@ -237,14 +219,7 @@ function MakeCustomField({ row, column, table }: CellContext<ExtendedEntry, unkn
|
|||||||
return <GhostedText multiline>{initialValue}</GhostedText>;
|
return <GhostedText multiline>{initialValue}</GhostedText>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||||
<MultiLineCell
|
|
||||||
initialValue={initialValue}
|
|
||||||
fieldId={column.id}
|
|
||||||
fieldLabel={getColumnLabel(column)}
|
|
||||||
handleUpdate={update}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,5 +48,4 @@ $max-playback-width: 30rem;
|
|||||||
flex: 0 0 $overview-width;
|
flex: 0 0 $overview-width;
|
||||||
min-width: $overview-width;
|
min-width: $overview-width;
|
||||||
max-width: $overview-width;
|
max-width: $overview-width;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
|
||||||
background: $bg-container-l1;
|
background: $bg-container-l1;
|
||||||
padding-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ export default function StudioLoader() {
|
|||||||
return <Studio {...data} />;
|
return <Studio {...data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Studio({ customFields, projectData, isMirrored, settings, viewSettings }: StudioData) {
|
function Studio({ projectData, isMirrored, settings, viewSettings }: StudioData) {
|
||||||
const { hideCards } = useStudioOptions();
|
const { hideCards } = useStudioOptions();
|
||||||
|
|
||||||
// gather option data
|
// gather option data
|
||||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||||
const studioOptions = useMemo(() => getStudioOptions(defaultFormat, customFields), [defaultFormat, customFields]);
|
const studioOptions = useMemo(() => getStudioOptions(defaultFormat), [defaultFormat]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(['studio', isMirrored && 'mirror'])} data-testid='studio-view'>
|
<div className={cx(['studio', isMirrored && 'mirror'])} data-testid='studio-view'>
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import { useAuxTimersTime, useStudioTimersSocket } from '../../common/hooks/useS
|
|||||||
import { getOffsetState } from '../../common/utils/offset';
|
import { getOffsetState } from '../../common/utils/offset';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import { getPropertyValue } from '../common/viewUtils';
|
|
||||||
import { getTimerColour } from '../utils/presentation.utils';
|
import { getTimerColour } from '../utils/presentation.utils';
|
||||||
|
|
||||||
import { useStudioOptions } from './studio.options';
|
|
||||||
import { getFormattedEventData, getFormattedScheduleTimes } from './studioTimers.utils';
|
import { getFormattedEventData, getFormattedScheduleTimes } from './studioTimers.utils';
|
||||||
|
|
||||||
import './StudioTimers.scss';
|
import './StudioTimers.scss';
|
||||||
@@ -19,7 +17,6 @@ interface StudioTimersProps {
|
|||||||
|
|
||||||
export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const { mainSource } = useStudioOptions();
|
|
||||||
const { eventNow, eventNext, message, time, offset, rundown, expectedRundownEnd } = useStudioTimersSocket();
|
const { eventNow, eventNext, message, time, offset, rundown, expectedRundownEnd } = useStudioTimersSocket();
|
||||||
|
|
||||||
const schedule = getFormattedScheduleTimes({
|
const schedule = getFormattedScheduleTimes({
|
||||||
@@ -27,8 +24,8 @@ export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
|||||||
actualStart: rundown.actualStart,
|
actualStart: rundown.actualStart,
|
||||||
expectedEnd: expectedRundownEnd,
|
expectedEnd: expectedRundownEnd,
|
||||||
});
|
});
|
||||||
const event = getFormattedEventData(eventNow, time, mainSource);
|
const event = getFormattedEventData(eventNow, time);
|
||||||
const eventNextTitle = getPropertyValue(eventNext, mainSource ?? 'title') || '-';
|
const eventNextTitle = eventNext?.title || '-';
|
||||||
const formattedTimerMessage = (message.timer.visible && message.timer.text) || '-';
|
const formattedTimerMessage = (message.timer.visible && message.timer.text) || '-';
|
||||||
const formattedSecondaryMessage = message.timer.secondarySource === 'secondary' ? message.secondary || '-' : '-';
|
const formattedSecondaryMessage = message.timer.secondarySource === 'secondary' ? message.secondary || '-' : '-';
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,30 @@
|
|||||||
import { use, useMemo } from 'react';
|
import { use, useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
import { CustomFields, OntimeEvent } from 'ontime-types';
|
|
||||||
|
|
||||||
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
|
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
|
||||||
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import { isStringBoolean } from '../common/viewUtils';
|
import { isStringBoolean } from '../common/viewUtils';
|
||||||
|
|
||||||
export const getStudioOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
export const getStudioOptions = (timeFormat: string): ViewOption[] => [
|
||||||
const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]);
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
|
{
|
||||||
return [
|
title: OptionTitle.ElementVisibility,
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
collapsible: true,
|
||||||
{
|
options: [
|
||||||
title: OptionTitle.DataSources,
|
{
|
||||||
collapsible: true,
|
id: 'hideCards',
|
||||||
options: [
|
title: 'Hide cards section',
|
||||||
{
|
description: 'Hides the card section with the timers',
|
||||||
id: 'main',
|
type: 'boolean',
|
||||||
title: 'Main text',
|
defaultValue: false,
|
||||||
description: 'Select the data source for the main text',
|
},
|
||||||
type: 'option',
|
],
|
||||||
values: mainOptions,
|
},
|
||||||
defaultValue: 'title',
|
];
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: OptionTitle.ElementVisibility,
|
|
||||||
collapsible: true,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
id: 'hideCards',
|
|
||||||
title: 'Hide cards section',
|
|
||||||
description: 'Hides the card section with the timers',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
type StudioOptions = {
|
type StudioOptions = {
|
||||||
mainSource: keyof OntimeEvent | null;
|
|
||||||
hideCards: boolean;
|
hideCards: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +37,6 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
|
|||||||
const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key);
|
const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mainSource: getValue('main') as keyof OntimeEvent | null,
|
|
||||||
hideCards: isStringBoolean(getValue('hideCards')),
|
hideCards: isStringBoolean(getValue('hideCards')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,9 @@ export function getFormattedScheduleTimes(data: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFormattedEventData(
|
export function getFormattedEventData(eventNow: OntimeEvent | null, timer: TimerState) {
|
||||||
eventNow: OntimeEvent | null,
|
|
||||||
timer: TimerState,
|
|
||||||
mainSource: keyof OntimeEvent | null,
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
title: (eventNow?.[mainSource ?? 'title'] as string) || '-',
|
title: eventNow?.title || '-',
|
||||||
startedAt: formatTime(timer.startedAt, timeFormat),
|
startedAt: formatTime(timer.startedAt, timeFormat),
|
||||||
expectedEnd: formatTime(timer.expectedFinish, timeFormat),
|
expectedEnd: formatTime(timer.expectedFinish, timeFormat),
|
||||||
timer: millisToString(timer.current),
|
timer: millisToString(timer.current),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { CustomFields, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
import { ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
@@ -8,7 +7,6 @@ import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
|||||||
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
||||||
|
|
||||||
export interface StudioData {
|
export interface StudioData {
|
||||||
customFields: CustomFields;
|
|
||||||
projectData: ProjectData;
|
projectData: ProjectData;
|
||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@@ -23,16 +21,14 @@ export function useStudioData(): ViewData<StudioData> {
|
|||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
customFields,
|
|
||||||
projectData,
|
projectData,
|
||||||
isMirrored,
|
isMirrored,
|
||||||
settings,
|
settings,
|
||||||
viewSettings,
|
viewSettings,
|
||||||
},
|
},
|
||||||
status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus, customFieldsStatus]),
|
status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
|||||||
import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent';
|
import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent';
|
||||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { getPropertyValue } from '../common/viewUtils';
|
|
||||||
|
|
||||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||||
import { useTimelineOptions } from './timeline.options';
|
import { useTimelineOptions } from './timeline.options';
|
||||||
@@ -25,7 +24,7 @@ interface TimelineProps {
|
|||||||
export default memo(Timeline);
|
export default memo(Timeline);
|
||||||
function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: TimelineProps) {
|
function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: TimelineProps) {
|
||||||
const { width: screenWidth } = useViewportSize();
|
const { width: screenWidth } = useViewportSize();
|
||||||
const { mainSource, hidePast, fixedSize } = useTimelineOptions();
|
const { hidePast, fixedSize } = useTimelineOptions();
|
||||||
const selectedRef = useRef<HTMLDivElement>(null);
|
const selectedRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -87,8 +86,6 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
|
|||||||
const position = positions[index];
|
const position = positions[index];
|
||||||
if (!position) return null;
|
if (!position) return null;
|
||||||
|
|
||||||
const displayTitle = getPropertyValue(event, mainSource ?? 'title') || event.title;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimelineEntry
|
<TimelineEntry
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@@ -103,7 +100,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
|
|||||||
totalGap={event.totalGap}
|
totalGap={event.totalGap}
|
||||||
isLinkedToLoaded={event.isLinkedToLoaded}
|
isLinkedToLoaded={event.isLinkedToLoaded}
|
||||||
dayOffset={event.dayOffset}
|
dayOffset={event.dayOffset}
|
||||||
title={displayTitle}
|
title={event.title}
|
||||||
cue={event.cue}
|
cue={event.cue}
|
||||||
width={position.width}
|
width={position.width}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Loader from '../common/loader/Loader';
|
|||||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import Timeline from './Timeline';
|
import Timeline from './Timeline';
|
||||||
import { getTimelineOptions, useTimelineOptions } from './timeline.options';
|
import { getTimelineOptions } from './timeline.options';
|
||||||
import { getUpcomingEvents, useScopedRundown } from './timeline.utils';
|
import { getUpcomingEvents, useScopedRundown } from './timeline.utils';
|
||||||
import TimelineSections from './TimelineSections';
|
import TimelineSections from './TimelineSections';
|
||||||
import { TimelineData, useTimelineData } from './useTimelineData';
|
import { TimelineData, useTimelineData } from './useTimelineData';
|
||||||
@@ -35,9 +35,8 @@ export default function TimelinePageLoader() {
|
|||||||
return <TimelinePage {...data} />;
|
return <TimelinePage {...data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelinePage({ events, customFields, projectData, settings }: TimelineData) {
|
function TimelinePage({ events, projectData, settings }: TimelineData) {
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const { mainSource } = useTimelineOptions();
|
|
||||||
// holds copy of the rundown with only relevant events
|
// holds copy of the rundown with only relevant events
|
||||||
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId);
|
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId);
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
|
|||||||
|
|
||||||
// populate options
|
// populate options
|
||||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||||
const progressOptions = useMemo(() => getTimelineOptions(defaultFormat, customFields), [defaultFormat, customFields]);
|
const progressOptions = useMemo(() => getTimelineOptions(defaultFormat), [defaultFormat]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='timeline' data-testid='timeline-view'>
|
<div className='timeline' data-testid='timeline-view'>
|
||||||
@@ -59,7 +58,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
|
|||||||
<TimelineClock />
|
<TimelineClock />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TimelineSections now={now} next={next} followedBy={followedBy} mainSource={mainSource} />
|
<TimelineSections now={now} next={next} followedBy={followedBy} />
|
||||||
|
|
||||||
<Timeline
|
<Timeline
|
||||||
firstStart={firstStart}
|
firstStart={firstStart}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useExpectedStartData } from '../../common/hooks/useSocket';
|
|||||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { formatDuration, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
|
import { formatDuration, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import { getPropertyValue } from '../common/viewUtils';
|
|
||||||
|
|
||||||
import TimelineSection from './timeline-section/TimelineSection';
|
import TimelineSection from './timeline-section/TimelineSection';
|
||||||
|
|
||||||
@@ -13,18 +12,17 @@ interface TimelineSectionsProps {
|
|||||||
now: ExtendedEntry<OntimeEvent> | null;
|
now: ExtendedEntry<OntimeEvent> | null;
|
||||||
next: ExtendedEntry<OntimeEvent> | null;
|
next: ExtendedEntry<OntimeEvent> | null;
|
||||||
followedBy: ExtendedEntry<OntimeEvent> | null;
|
followedBy: ExtendedEntry<OntimeEvent> | null;
|
||||||
mainSource: keyof OntimeEvent | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TimelineSections({ now, next, followedBy, mainSource }: TimelineSectionsProps) {
|
export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const state = useExpectedStartData();
|
const state = useExpectedStartData();
|
||||||
|
|
||||||
// gather card data
|
// gather card data
|
||||||
const titleNow = getPropertyValue(now, mainSource ?? 'title') ?? '-';
|
const titleNow = now?.title ?? '-';
|
||||||
const dueText = getLocalizedString('timeline.due').toUpperCase();
|
const dueText = getLocalizedString('timeline.due').toUpperCase();
|
||||||
const nextText = getPropertyValue(next, mainSource ?? 'title') ?? '-';
|
const nextText = next !== null ? next.title : '-';
|
||||||
const followedByText = getPropertyValue(followedBy, mainSource ?? 'title') ?? '-';
|
const followedByText = followedBy !== null ? followedBy.title : '-';
|
||||||
let nextStatus: string | undefined;
|
let nextStatus: string | undefined;
|
||||||
let followedByStatus: string | undefined;
|
let followedByStatus: string | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,15 @@
|
|||||||
import { use, useMemo } from 'react';
|
import { use, useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
import { CustomFields, OntimeEvent } from 'ontime-types';
|
|
||||||
|
|
||||||
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
|
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
|
||||||
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import { isStringBoolean } from '../common/viewUtils';
|
import { isStringBoolean } from '../common/viewUtils';
|
||||||
|
|
||||||
export const getTimelineOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
export const getTimelineOptions = (timeFormat: string): ViewOption[] => {
|
||||||
const mainOptions = makeOptionsFromCustomFields(customFields, [{ value: 'title', label: 'Title' }]);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
{
|
|
||||||
title: OptionTitle.DataSources,
|
|
||||||
collapsible: true,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
id: 'main',
|
|
||||||
title: 'Main text',
|
|
||||||
description: 'Select the data source for the main text',
|
|
||||||
type: 'option',
|
|
||||||
values: mainOptions,
|
|
||||||
defaultValue: 'title',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: OptionTitle.ElementVisibility,
|
title: OptionTitle.ElementVisibility,
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
@@ -52,7 +34,6 @@ export const getTimelineOptions = (timeFormat: string, customFields: CustomField
|
|||||||
};
|
};
|
||||||
|
|
||||||
type TimelineOptions = {
|
type TimelineOptions = {
|
||||||
mainSource: keyof OntimeEvent | null;
|
|
||||||
hidePast: boolean;
|
hidePast: boolean;
|
||||||
fixedSize: boolean;
|
fixedSize: boolean;
|
||||||
};
|
};
|
||||||
@@ -66,7 +47,6 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
|
|||||||
const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key);
|
const getValue = (key: string) => defaultValues?.get(key) ?? searchParams.get(key);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mainSource: getValue('main') as keyof OntimeEvent | null,
|
|
||||||
hidePast: isStringBoolean(getValue('hidePast')),
|
hidePast: isStringBoolean(getValue('hidePast')),
|
||||||
fixedSize: isStringBoolean(getValue('fixedSize')),
|
fixedSize: isStringBoolean(getValue('fixedSize')),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { CustomFields, OntimeEntry, ProjectData, Settings } from 'ontime-types';
|
import { OntimeEntry, ProjectData, Settings } from 'ontime-types';
|
||||||
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import { useFlatRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
import { useFlatRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
@@ -9,7 +8,6 @@ import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
|||||||
|
|
||||||
export interface TimelineData {
|
export interface TimelineData {
|
||||||
events: ExtendedEntry<OntimeEntry>[];
|
events: ExtendedEntry<OntimeEntry>[];
|
||||||
customFields: CustomFields;
|
|
||||||
projectData: ProjectData;
|
projectData: ProjectData;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
@@ -19,15 +17,13 @@ export function useTimelineData(): ViewData<TimelineData> {
|
|||||||
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
events: rundownData,
|
events: rundownData,
|
||||||
customFields,
|
|
||||||
projectData,
|
projectData,
|
||||||
settings,
|
settings,
|
||||||
},
|
},
|
||||||
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]),
|
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function TimerLoader() {
|
|||||||
return <Timer {...data} />;
|
return <Timer {...data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Timer({ customFields, projectData, isMirrored, settings, viewSettings, entries }: TimerData) {
|
function Timer({ customFields, projectData, isMirrored, settings, viewSettings }: TimerData) {
|
||||||
const { eventNext, eventNow, message, time, clock, timerTypeNow, countToEndNow, auxTimer } = useTimerSocket();
|
const { eventNext, eventNow, message, time, clock, timerTypeNow, countToEndNow, auxTimer } = useTimerSocket();
|
||||||
const {
|
const {
|
||||||
hideClock,
|
hideClock,
|
||||||
@@ -97,7 +97,6 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings,
|
|||||||
secondarySource,
|
secondarySource,
|
||||||
time.playback,
|
time.playback,
|
||||||
time.phase,
|
time.phase,
|
||||||
entries,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// gather timer data
|
// gather timer data
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
|
|||||||
const mainOptions = makeOptionsFromCustomFields(customFields, [
|
const mainOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
{ value: 'none', label: 'None' },
|
{ value: 'none', label: 'None' },
|
||||||
{ value: 'title', label: 'Title' },
|
{ value: 'title', label: 'Title' },
|
||||||
|
{ value: 'note', label: 'Note' },
|
||||||
]);
|
]);
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
{ value: 'none', label: 'None' },
|
{ value: 'none', label: 'None' },
|
||||||
{ value: 'title', label: 'Title' },
|
{ value: 'title', label: 'Title' },
|
||||||
{ value: 'note', label: 'Note' },
|
{ value: 'note', label: 'Note' },
|
||||||
{ value: 'parent', label: 'Group Title' },
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -185,8 +185,8 @@ type TimerOptions = {
|
|||||||
hideLogo: boolean;
|
hideLogo: boolean;
|
||||||
hideTimerSeconds: boolean;
|
hideTimerSeconds: boolean;
|
||||||
removeLeadingZeros: boolean;
|
removeLeadingZeros: boolean;
|
||||||
mainSource: keyof OntimeEvent | null | 'none';
|
mainSource: keyof OntimeEvent | null;
|
||||||
secondarySource: keyof OntimeEvent | null | 'none';
|
secondarySource: keyof OntimeEvent | null;
|
||||||
timerType?: TimerType;
|
timerType?: TimerType;
|
||||||
freezeOvertime: boolean;
|
freezeOvertime: boolean;
|
||||||
freezeMessage: string;
|
freezeMessage: string;
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
import {
|
import { MaybeNumber, MessageState, OntimeEvent, Playback, TimerMessage, TimerPhase, TimerType } from 'ontime-types';
|
||||||
MaybeNumber,
|
|
||||||
MessageState,
|
|
||||||
OntimeEvent,
|
|
||||||
Playback,
|
|
||||||
RundownEntries,
|
|
||||||
TimerMessage,
|
|
||||||
TimerPhase,
|
|
||||||
TimerType,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { isPlaybackActive } from 'ontime-utils';
|
import { isPlaybackActive } from 'ontime-utils';
|
||||||
|
|
||||||
import { getFormattedTimer, getPropertyValue } from '../common/viewUtils';
|
import { getFormattedTimer, getPropertyValue } from '../common/viewUtils';
|
||||||
@@ -150,11 +141,10 @@ export function getSecondaryDisplay(
|
|||||||
export function getCardData(
|
export function getCardData(
|
||||||
eventNow: OntimeEvent | null,
|
eventNow: OntimeEvent | null,
|
||||||
eventNext: OntimeEvent | null,
|
eventNext: OntimeEvent | null,
|
||||||
mainSource: keyof OntimeEvent | null | 'none',
|
mainSource: keyof OntimeEvent | null,
|
||||||
secondarySource: keyof OntimeEvent | null | 'none',
|
secondarySource: keyof OntimeEvent | null,
|
||||||
playback: Playback,
|
playback: Playback,
|
||||||
phase: TimerPhase,
|
phase: TimerPhase,
|
||||||
entries: RundownEntries,
|
|
||||||
) {
|
) {
|
||||||
if (playback === Playback.Stop) {
|
if (playback === Playback.Stop) {
|
||||||
return {
|
return {
|
||||||
@@ -172,19 +162,19 @@ export function getCardData(
|
|||||||
|
|
||||||
// if we are loaded, we show the upcoming event as next
|
// if we are loaded, we show the upcoming event as next
|
||||||
const nowMain = hasActiveTimer ? getPropertyValue(eventNow, mainSource ?? 'title') : undefined;
|
const nowMain = hasActiveTimer ? getPropertyValue(eventNow, mainSource ?? 'title') : undefined;
|
||||||
const nowSecondary = hasActiveTimer ? getPropertyValue(eventNow, secondarySource, entries) : undefined;
|
const nowSecondary = hasActiveTimer ? getPropertyValue(eventNow, secondarySource) : undefined;
|
||||||
const nextMain = hasActiveTimer
|
const nextMain = hasActiveTimer
|
||||||
? getPropertyValue(eventNext, mainSource ?? 'title')
|
? getPropertyValue(eventNext, mainSource ?? 'title')
|
||||||
: getPropertyValue(eventNow, mainSource ?? 'title');
|
: getPropertyValue(eventNow, mainSource ?? 'title');
|
||||||
const nextSecondary = hasActiveTimer
|
const nextSecondary = hasActiveTimer
|
||||||
? getPropertyValue(eventNext, secondarySource, entries)
|
? getPropertyValue(eventNext, secondarySource)
|
||||||
: getPropertyValue(eventNow, secondarySource, entries);
|
: getPropertyValue(eventNow, secondarySource);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showNow: mainSource !== 'none' && (Boolean(nowMain) || Boolean(nowSecondary)),
|
showNow: Boolean(nowMain) || Boolean(nowSecondary),
|
||||||
nowMain,
|
nowMain,
|
||||||
nowSecondary,
|
nowSecondary,
|
||||||
showNext: mainSource !== 'none' && (Boolean(nextMain) || Boolean(nextSecondary)),
|
showNext: Boolean(nextMain) || Boolean(nextSecondary),
|
||||||
nextMain,
|
nextMain,
|
||||||
nextSecondary,
|
nextSecondary,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { CustomFields, ProjectData, RundownEntries, Settings, ViewSettings } from 'ontime-types';
|
import { CustomFields, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
@@ -14,7 +13,6 @@ export interface TimerData {
|
|||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
entries: RundownEntries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTimerData(): ViewData<TimerData> {
|
export function useTimerData(): ViewData<TimerData> {
|
||||||
@@ -26,8 +24,6 @@ export function useTimerData(): ViewData<TimerData> {
|
|||||||
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
||||||
const { data: rundown, status: rundownStatus } = useRundown();
|
|
||||||
const { entries } = rundown;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -36,14 +32,7 @@ export function useTimerData(): ViewData<TimerData> {
|
|||||||
isMirrored,
|
isMirrored,
|
||||||
settings,
|
settings,
|
||||||
viewSettings,
|
viewSettings,
|
||||||
entries,
|
|
||||||
},
|
},
|
||||||
status: aggregateQueryStatus([
|
status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus, customFieldsStatus]),
|
||||||
projectDataStatus,
|
|
||||||
viewSettingsStatus,
|
|
||||||
settingsStatus,
|
|
||||||
customFieldsStatus,
|
|
||||||
rundownStatus,
|
|
||||||
]),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-electron",
|
"name": "ontime-electron",
|
||||||
"version": "4.4.2",
|
"version": "4.4.0-beta.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/resolver",
|
"name": "@getontime/resolver",
|
||||||
"version": "4.4.2",
|
"version": "4.4.0-beta.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
"types": "./dist/main.d.ts",
|
"types": "./dist/main.d.ts",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "4.4.2",
|
"version": "4.4.0-beta.0",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import * as oscClient from '../clients/osc.client.js';
|
|||||||
import * as httpClient from '../clients/http.client.js';
|
import * as httpClient from '../clients/http.client.js';
|
||||||
|
|
||||||
import { makeOSCAction, makeHTTPAction } from './testUtils.js';
|
import { makeOSCAction, makeHTTPAction } from './testUtils.js';
|
||||||
import { RuntimeState } from '../../../stores/runtimeState.js';
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
|
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
|
||||||
@@ -101,440 +100,117 @@ describe('testConditions()', () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('equals', () => {
|
describe('equals operator', () => {
|
||||||
test.each([
|
it('should compare two equal values', () => {
|
||||||
{
|
const mockStore = makeRuntimeStateData({ clock: 10 });
|
||||||
description: 'number and string number',
|
const result = testConditions([{ field: 'clock', operator: 'equals', value: '10' }], 'all', mockStore);
|
||||||
value: '10',
|
expect(result).toBe(true);
|
||||||
state: 10,
|
});
|
||||||
},
|
|
||||||
{
|
it('string comparisons should be case insensitive', () => {
|
||||||
description: 'string and string',
|
const mockStore = makeRuntimeStateData({ eventNow: { title: 'Title' } as PlayableEvent });
|
||||||
value: 'Title',
|
const result = testConditions(
|
||||||
state: 'Title',
|
[{ field: 'eventNow.title', operator: 'equals', value: 'title' }],
|
||||||
},
|
'all',
|
||||||
{
|
mockStore,
|
||||||
description: 'null and empty string',
|
);
|
||||||
value: '',
|
expect(result).toBe(true);
|
||||||
state: null,
|
});
|
||||||
},
|
|
||||||
{
|
it('should check if a value does not exist', () => {
|
||||||
description: 'undefined and empty string',
|
const mockStore = makeRuntimeStateData({ eventNow: null });
|
||||||
value: '',
|
const result = testConditions([{ field: 'eventNow.title', operator: 'equals', value: '' }], 'all', mockStore);
|
||||||
state: undefined,
|
expect(result).toBe(true);
|
||||||
},
|
});
|
||||||
{
|
|
||||||
description: 'boolean true and string true',
|
it('should handle trueness boolean comparisons', () => {
|
||||||
value: 'true',
|
const mockStore = makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent });
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean false equals string false',
|
|
||||||
value: 'false',
|
|
||||||
state: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'not case sensitive for strings',
|
|
||||||
value: 'title',
|
|
||||||
state: 'Title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'not case sensitive for booleans',
|
|
||||||
value: 'TRUE',
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
])('$description', ({ value, state }) => {
|
|
||||||
expect(
|
expect(
|
||||||
testConditions([{ field: 'test.path', operator: 'equals', value }], 'all', {
|
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'true' }], 'all', mockStore),
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
testConditions([{ field: 'test.path', operator: 'not_equals', value }], 'all', {
|
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'false' }], 'all', mockStore),
|
||||||
test: { path: state },
|
).toBe(false);
|
||||||
} as unknown as RuntimeState),
|
});
|
||||||
|
|
||||||
|
it('should handle falseness boolean comparisons', () => {
|
||||||
|
const mockStore = makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent });
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'false' }], 'all', mockStore),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'true' }], 'all', mockStore),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('not_equals', () => {
|
describe('not_equals operator', () => {
|
||||||
test.each([
|
it('should check if two values are different', () => {
|
||||||
{
|
const mockStore = makeRuntimeStateData({ clock: 10 });
|
||||||
description: 'boolean true and string false',
|
const result = testConditions([{ field: 'clock', operator: 'not_equals', value: '11' }], 'all', mockStore);
|
||||||
value: 'false',
|
expect(result).toBe(true);
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean false and string true',
|
|
||||||
value: 'true',
|
|
||||||
state: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'number and non-numeric string',
|
|
||||||
value: 'lighting',
|
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'number and different number',
|
|
||||||
value: '100',
|
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '10 and 11',
|
|
||||||
value: '11',
|
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '10 and 5',
|
|
||||||
value: '5',
|
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '10 and 1',
|
|
||||||
value: '1',
|
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and substring',
|
|
||||||
value: 'lighting',
|
|
||||||
state: 'testing-lighting-10',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and different string',
|
|
||||||
value: 'sound',
|
|
||||||
state: 'testing-lighting-10',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'null and non-empty string',
|
|
||||||
value: 'not-empty',
|
|
||||||
state: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'undefined and non-empty string',
|
|
||||||
value: 'not-empty',
|
|
||||||
state: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'undefined and " " string',
|
|
||||||
value: ' ',
|
|
||||||
state: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean false and empty string',
|
|
||||||
value: '',
|
|
||||||
state: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '0 and empty string',
|
|
||||||
value: '',
|
|
||||||
state: 0,
|
|
||||||
},
|
|
||||||
])('$description', ({ value, state }) => {
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'not_equals', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'equals', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('greater_than', () => {
|
describe('greater_than operator', () => {
|
||||||
test.each([
|
it('should check if the given value is smaller', () => {
|
||||||
{
|
const mockStore = makeRuntimeStateData({ clock: 10 });
|
||||||
description: '10 > 5',
|
const result = testConditions([{ field: 'clock', operator: 'greater_than', value: '9' }], 'all', mockStore);
|
||||||
value: '5',
|
expect(result).toBe(true);
|
||||||
state: 10,
|
});
|
||||||
},
|
it('should handle values which are not numbers', () => {
|
||||||
{
|
const mockStore = makeRuntimeStateData({ clock: 10 });
|
||||||
description: '10 > 1',
|
const result = testConditions([{ field: 'clock', operator: 'greater_than', value: 'testing' }], 'all', mockStore);
|
||||||
value: '1',
|
expect(result).toBe(false);
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '100 > 5',
|
|
||||||
value: '5',
|
|
||||||
state: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '11 > 10',
|
|
||||||
value: '10',
|
|
||||||
state: 11,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '0 > -1',
|
|
||||||
value: '-1',
|
|
||||||
state: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '-100 > -150',
|
|
||||||
value: '-150',
|
|
||||||
state: -100,
|
|
||||||
},
|
|
||||||
])('$description', ({ value, state }) => {
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'greater_than', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'less_than', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('less_than', () => {
|
describe('less_than operator', () => {
|
||||||
test.each([
|
it('should check if the given value is larger', () => {
|
||||||
{
|
const mockStore = makeRuntimeStateData({ clock: 10 });
|
||||||
description: '10 > 5',
|
const result = testConditions([{ field: 'clock', operator: 'less_than', value: '11' }], 'all', mockStore);
|
||||||
value: '10',
|
expect(result).toBe(true);
|
||||||
state: 5,
|
});
|
||||||
},
|
it('should handle values which are not numbers', () => {
|
||||||
{
|
const mockStore = makeRuntimeStateData({ clock: 10 });
|
||||||
description: '10 > 1',
|
const result = testConditions([{ field: 'clock', operator: 'less_than', value: 'testing' }], 'all', mockStore);
|
||||||
value: '10',
|
expect(result).toBe(false);
|
||||||
state: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '100 > 5',
|
|
||||||
value: '100',
|
|
||||||
state: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '11 > 10',
|
|
||||||
value: '11',
|
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '0 > -1',
|
|
||||||
value: '0',
|
|
||||||
state: -1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: '-100 > -150',
|
|
||||||
value: '-100',
|
|
||||||
state: -150,
|
|
||||||
},
|
|
||||||
])('$description', ({ value, state }) => {
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'less_than', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'greater_than', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid and edge case for greater/less_than', () => {
|
describe('contains operator', () => {
|
||||||
test.each([
|
it('should check if value contains given string', () => {
|
||||||
{
|
const result = testConditions(
|
||||||
description: 'number and non-numeric string',
|
[{ field: 'eventNow.title', operator: 'contains', value: 'lighting' }],
|
||||||
value: 'lighting',
|
'all',
|
||||||
state: 10,
|
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
||||||
},
|
);
|
||||||
{
|
expect(result).toBe(true);
|
||||||
description: '0 and empty string',
|
|
||||||
value: '',
|
const result2 = testConditions(
|
||||||
state: 0,
|
[{ field: 'eventNow.title', operator: 'contains', value: 'sound' }],
|
||||||
},
|
'all',
|
||||||
{
|
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
||||||
description: 'equal values',
|
);
|
||||||
value: '10',
|
expect(result2).toBe(false);
|
||||||
state: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean false and empty string',
|
|
||||||
value: '',
|
|
||||||
state: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'undefined and non-empty string',
|
|
||||||
value: 'not-empty',
|
|
||||||
state: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'null and non-empty string',
|
|
||||||
value: 'not-empty',
|
|
||||||
state: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and different string',
|
|
||||||
value: 'sound',
|
|
||||||
state: 'testing-lighting-10',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and substring',
|
|
||||||
value: 'lighting',
|
|
||||||
state: 'testing-lighting-10',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean true and uppercase TRUE',
|
|
||||||
value: 'TRUE',
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and lowercase string',
|
|
||||||
value: 'title',
|
|
||||||
state: 'Title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean false and string true',
|
|
||||||
value: 'true',
|
|
||||||
state: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean false and string false',
|
|
||||||
value: 'false',
|
|
||||||
state: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean true and string false',
|
|
||||||
value: 'false',
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and string',
|
|
||||||
value: 'Title',
|
|
||||||
state: 'Title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'null and empty string',
|
|
||||||
value: '',
|
|
||||||
state: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'undefined and empty string',
|
|
||||||
value: '',
|
|
||||||
state: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean true and string true',
|
|
||||||
value: 'true',
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
])('$description', ({ value, state }) => {
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'less_than', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(false);
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'greater_than', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('contains', () => {
|
describe('not_contains operator', () => {
|
||||||
test.each([
|
it('should check if value does not contain given string', () => {
|
||||||
{
|
const result = testConditions(
|
||||||
description: 'substring in string',
|
[{ field: 'eventNow.title', operator: 'not_contains', value: 'lighting' }],
|
||||||
value: 'lighting',
|
'all',
|
||||||
state: 'testing-lighting-10',
|
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
||||||
},
|
);
|
||||||
{
|
expect(result).toBe(false);
|
||||||
description: 'number in string',
|
|
||||||
value: '10',
|
|
||||||
state: 'testing-lighting-10',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'digit in string',
|
|
||||||
value: '1',
|
|
||||||
state: 'testing-lighting-10',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'empty in empty',
|
|
||||||
value: '',
|
|
||||||
state: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'empty in not-empty',
|
|
||||||
value: '',
|
|
||||||
state: '12345', // TODO: is this intended
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string equals string',
|
|
||||||
value: 'Title',
|
|
||||||
state: 'Title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'case sensitive',
|
|
||||||
value: 'title',
|
|
||||||
state: 'Title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'number does contain string',
|
|
||||||
value: '10',
|
|
||||||
state: 2105,
|
|
||||||
},
|
|
||||||
])('$description', ({ value, state }) => {
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'contains', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'not_contains', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('not_contains', () => {
|
const result2 = testConditions(
|
||||||
test.each([
|
[{ field: 'eventNow.title', operator: 'not_contains', value: 'sound' }],
|
||||||
{
|
'all',
|
||||||
description: 'number does not contain substring',
|
makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }),
|
||||||
value: '456',
|
);
|
||||||
state: 12345,
|
expect(result2).toBe(true);
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'substring not in string',
|
|
||||||
value: 'sound',
|
|
||||||
state: 'testing-lighting-10',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and undefined',
|
|
||||||
value: 'sound',
|
|
||||||
state: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'string and null',
|
|
||||||
value: 'sound',
|
|
||||||
state: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean true and string true',
|
|
||||||
value: 'true',
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'boolean false and string false',
|
|
||||||
value: 'false',
|
|
||||||
state: false,
|
|
||||||
},
|
|
||||||
])('$description', ({ value, state }) => {
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'not_contains', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
testConditions([{ field: 'test.path', operator: 'contains', value }], 'all', {
|
|
||||||
test: { path: state },
|
|
||||||
} as unknown as RuntimeState),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { isOntimeCloud } from '../../setup/environment.js';
|
|||||||
import { emitOSC } from './clients/osc.client.js';
|
import { emitOSC } from './clients/osc.client.js';
|
||||||
import { emitHTTP } from './clients/http.client.js';
|
import { emitHTTP } from './clients/http.client.js';
|
||||||
import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js';
|
import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js';
|
||||||
import { isContained, isEquivalent, isGreaterThan, isLessThan } from './automation.utils.js';
|
import { isBooleanEquals, isGreaterThan, isLessThan } from './automation.utils.js';
|
||||||
import { toOntimeAction } from './clients/ontime.client.js';
|
import { toOntimeAction } from './clients/ontime.client.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,17 +92,29 @@ export function testConditions(
|
|||||||
// we use loose equality to be able to check for converted values (eg '10' == 10)
|
// we use loose equality to be able to check for converted values (eg '10' == 10)
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case 'equals':
|
case 'equals':
|
||||||
return isEquivalent(fieldValue, lowerCasedValue);
|
// handle the case where we are comparing boolean strings
|
||||||
|
if (typeof fieldValue === 'boolean') {
|
||||||
|
return isBooleanEquals(fieldValue, lowerCasedValue);
|
||||||
|
}
|
||||||
|
// make string comparisons case insensitive
|
||||||
|
if (typeof fieldValue === 'string') {
|
||||||
|
return fieldValue.toLowerCase() === lowerCasedValue;
|
||||||
|
}
|
||||||
|
// overload the edge case where we use empty string to check if a value does not exist
|
||||||
|
if (value === '' && fieldValue === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return fieldValue == value;
|
||||||
case 'not_equals':
|
case 'not_equals':
|
||||||
return !isEquivalent(fieldValue, lowerCasedValue);
|
return fieldValue != value;
|
||||||
case 'greater_than':
|
case 'greater_than':
|
||||||
return isGreaterThan(fieldValue, value);
|
return isGreaterThan(fieldValue, value);
|
||||||
case 'less_than':
|
case 'less_than':
|
||||||
return isLessThan(fieldValue, value);
|
return isLessThan(fieldValue, value);
|
||||||
case 'contains':
|
case 'contains':
|
||||||
return isContained(fieldValue, lowerCasedValue);
|
return typeof fieldValue === 'string' && fieldValue.includes(value);
|
||||||
case 'not_contains':
|
case 'not_contains':
|
||||||
return !isContained(fieldValue, lowerCasedValue);
|
return typeof fieldValue === 'string' && !fieldValue.includes(value);
|
||||||
default: {
|
default: {
|
||||||
operator satisfies never;
|
operator satisfies never;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -148,38 +148,6 @@ const quickAliases: AliasesDefinition = {
|
|||||||
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
|
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isEquivalent(a: unknown, b: string): boolean {
|
|
||||||
// handle the case where we are comparing boolean strings
|
|
||||||
if (typeof a === 'boolean') {
|
|
||||||
return isBooleanEquals(a, b);
|
|
||||||
}
|
|
||||||
// make string comparisons case insensitive
|
|
||||||
if (typeof a === 'string') {
|
|
||||||
return a.toLowerCase() === b;
|
|
||||||
}
|
|
||||||
// overload the edge case where we use empty string to check if a value does not exist
|
|
||||||
// this also avoids the case where 0 == ''
|
|
||||||
if (b === '') {
|
|
||||||
return a === null || a === undefined;
|
|
||||||
}
|
|
||||||
return a == b;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isContained(a: unknown, b: string): boolean {
|
|
||||||
// handle the case where we are comparing boolean strings
|
|
||||||
if (typeof a === 'boolean') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// make string comparisons case insensitive
|
|
||||||
if (typeof a === 'string') {
|
|
||||||
return a.toLowerCase().includes(b);
|
|
||||||
}
|
|
||||||
if (typeof a === 'number') {
|
|
||||||
return a.toString().toLowerCase().includes(b);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility encapsulates logic for comparing two strings which may encode numbers
|
* Utility encapsulates logic for comparing two strings which may encode numbers
|
||||||
* @example isGreaterThan('10', '5') // true
|
* @example isGreaterThan('10', '5') // true
|
||||||
|
|||||||
@@ -17,15 +17,10 @@ import {
|
|||||||
URLPreset,
|
URLPreset,
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import { customFieldLabelToKey, checkRegex, isKnownTimerType, validateEndAction } from 'ontime-utils';
|
||||||
customFieldLabelToKey,
|
|
||||||
checkRegex,
|
|
||||||
isKnownTimerType,
|
|
||||||
validateEndAction,
|
|
||||||
eventDef as eventModel,
|
|
||||||
} from 'ontime-utils';
|
|
||||||
|
|
||||||
import { is } from '../../../utils/is.js';
|
import { is } from '../../../utils/is.js';
|
||||||
|
import { event as eventModel } from '../../../models/eventsDefinition.js';
|
||||||
import { ONTIME_VERSION } from '../../../ONTIME_VERSION.js';
|
import { ONTIME_VERSION } from '../../../ONTIME_VERSION.js';
|
||||||
import { getPartialProject } from '../../../models/dataModel.js';
|
import { getPartialProject } from '../../../models/dataModel.js';
|
||||||
|
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ describe('v3 to v4', () => {
|
|||||||
|
|
||||||
const expectedAutomation: AutomationSettings = {
|
const expectedAutomation: AutomationSettings = {
|
||||||
enabledAutomations: true,
|
enabledAutomations: true,
|
||||||
enabledOscIn: false,
|
enabledOscIn: true,
|
||||||
oscPortIn: 8888,
|
oscPortIn: 8888,
|
||||||
triggers: [
|
triggers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ import {
|
|||||||
isKnownTimerType,
|
isKnownTimerType,
|
||||||
validateTimerType,
|
validateTimerType,
|
||||||
validateEndAction,
|
validateEndAction,
|
||||||
makeString,
|
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { Prettify } from 'ts-essentials';
|
import { Prettify } from 'ts-essentials';
|
||||||
|
|
||||||
import { is } from '../../utils/is.js';
|
import { is } from '../../utils/is.js';
|
||||||
|
import { makeString } from '../../utils/parserUtils.js';
|
||||||
import { parseExcelDate } from '../../utils/time.js';
|
import { parseExcelDate } from '../../utils/time.js';
|
||||||
import { generateImportHandlers, getCustomFieldData, parseBooleanString, SheetMetadata } from './excel.utils.js';
|
import { generateImportHandlers, getCustomFieldData, parseBooleanString, SheetMetadata } from './excel.utils.js';
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ export function getCustomFieldData(
|
|||||||
// we lower case the excel key to make it easier to match
|
// we lower case the excel key to make it easier to match
|
||||||
const columnNameInExcel = importMap.custom[ontimeLabel].toLowerCase();
|
const columnNameInExcel = importMap.custom[ontimeLabel].toLowerCase();
|
||||||
const maybeExistingColour = existingCustomFields[keyInCustomFields]?.colour ?? '';
|
const maybeExistingColour = existingCustomFields[keyInCustomFields]?.colour ?? '';
|
||||||
const maybeExistingType = existingCustomFields[keyInCustomFields]?.type ?? 'text';
|
|
||||||
|
|
||||||
// 1. add the custom field to the merged custom fields
|
// 1. add the custom field to the merged custom fields
|
||||||
mergedCustomFields[keyInCustomFields] = {
|
mergedCustomFields[keyInCustomFields] = {
|
||||||
type: maybeExistingType,
|
type: 'text', // we currently only support text custom fields
|
||||||
colour: maybeExistingColour,
|
colour: maybeExistingColour,
|
||||||
label: ontimeLabel,
|
label: ontimeLabel,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TimeStrategy, EndAction, TimerType, OntimeEvent } from 'ontime-types';
|
import { TimeStrategy, EndAction, TimerType, OntimeEvent, OntimeGroup } from 'ontime-types';
|
||||||
import { createEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
import { assertType } from 'vitest';
|
import { assertType } from 'vitest';
|
||||||
|
|
||||||
@@ -7,9 +7,11 @@ import { demoDb } from '../../../models/demoProject.js';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
calculateDayOffset,
|
calculateDayOffset,
|
||||||
|
createEvent,
|
||||||
deleteById,
|
deleteById,
|
||||||
doesInvalidateMetadata,
|
doesInvalidateMetadata,
|
||||||
duplicateRundown,
|
duplicateRundown,
|
||||||
|
getInsertAfterId,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
makeDeepClone,
|
makeDeepClone,
|
||||||
} from '../rundown.utils.js';
|
} from '../rundown.utils.js';
|
||||||
@@ -224,6 +226,45 @@ describe('calculateDayOffset()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getInsertAfterId()', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeEvent({ id: '1', parent: null }),
|
||||||
|
'2': makeOntimeEvent({ id: '2', parent: null }),
|
||||||
|
group: makeOntimeGroup({ id: 'group', entries: ['31', '32'] }),
|
||||||
|
'31': makeOntimeEvent({ id: '31', parent: 'group' }),
|
||||||
|
'32': makeOntimeEvent({ id: '32', parent: 'group' }),
|
||||||
|
'4': makeOntimeEvent({ id: '4', parent: null }),
|
||||||
|
},
|
||||||
|
order: ['1', '2', 'group', '4'],
|
||||||
|
flatOrder: ['1', '2', 'group', '31', '32', '4'],
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns afterId if provided', () => {
|
||||||
|
expect(getInsertAfterId(rundown, null, 'b')).toBe('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if neither afterId nor beforeId is provided', () => {
|
||||||
|
expect(getInsertAfterId(rundown, null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if beforeId is not found', () => {
|
||||||
|
expect(getInsertAfterId(rundown, null, undefined, 'z')).toBeNull();
|
||||||
|
expect(getInsertAfterId(rundown, null, undefined, '1')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the previous id of an entry in the rundown', () => {
|
||||||
|
expect(getInsertAfterId(rundown, null, undefined, '2')).toBe('1');
|
||||||
|
expect(getInsertAfterId(rundown, null, undefined, '4')).toBe('group');
|
||||||
|
expect(getInsertAfterId(rundown, null, undefined, 'group')).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the previous id of an event in a group', () => {
|
||||||
|
expect(getInsertAfterId(rundown, rundown.entries.group as OntimeGroup, undefined, '31')).toBeNull();
|
||||||
|
expect(getInsertAfterId(rundown, rundown.entries.group as OntimeGroup, undefined, '32')).toBe('31');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('duplicateRundown', () => {
|
describe('duplicateRundown', () => {
|
||||||
it('duplicates a given rundown', () => {
|
it('duplicates a given rundown', () => {
|
||||||
const demoRundown = demoDb.rundowns['default'];
|
const demoRundown = demoDb.rundowns['default'];
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
Rundown,
|
Rundown,
|
||||||
InsertOptions,
|
InsertOptions,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { addToRundown, customFieldLabelToKey, getInsertAfterId, insertAtIndex, createGroup } from 'ontime-utils';
|
import { customFieldLabelToKey, insertAtIndex } from 'ontime-utils';
|
||||||
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { consoleError } from '../../utils/console.js';
|
import { consoleError } from '../../utils/console.js';
|
||||||
@@ -35,8 +35,10 @@ import type { RundownMetadata } from './rundown.types.js';
|
|||||||
import {
|
import {
|
||||||
applyPatchToEntry,
|
applyPatchToEntry,
|
||||||
cloneSimpleRundownEntry,
|
cloneSimpleRundownEntry,
|
||||||
|
createGroup,
|
||||||
deleteById,
|
deleteById,
|
||||||
doesInvalidateMetadata,
|
doesInvalidateMetadata,
|
||||||
|
getInsertAfterId,
|
||||||
getUniqueId,
|
getUniqueId,
|
||||||
makeDeepClone,
|
makeDeepClone,
|
||||||
} from './rundown.utils.js';
|
} from './rundown.utils.js';
|
||||||
@@ -108,6 +110,11 @@ export function createTransaction(options: TransactionOptions): Transaction {
|
|||||||
function commit(shouldProcess: boolean = true) {
|
function commit(shouldProcess: boolean = true) {
|
||||||
// if the rundown is mutable we persist the changes
|
// if the rundown is mutable we persist the changes
|
||||||
if (options.mutableRundown) {
|
if (options.mutableRundown) {
|
||||||
|
// schedule a database update
|
||||||
|
setImmediate(async () => {
|
||||||
|
await getDataProvider().setRundown(cachedRundown.id, cachedRundown);
|
||||||
|
});
|
||||||
|
|
||||||
// update fields which are agnostic of whether the rundown is processed
|
// update fields which are agnostic of whether the rundown is processed
|
||||||
cachedRundown.revision = cachedRundown.revision + 1;
|
cachedRundown.revision = cachedRundown.revision + 1;
|
||||||
cachedRundown.title = rundown.title;
|
cachedRundown.title = rundown.title;
|
||||||
@@ -128,17 +135,16 @@ export function createTransaction(options: TransactionOptions): Transaction {
|
|||||||
cachedRundown.flatOrder = metadata.flatEntryOrder;
|
cachedRundown.flatOrder = metadata.flatEntryOrder;
|
||||||
rundownMetadata = metadata;
|
rundownMetadata = metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist after all mutations are applied
|
|
||||||
getDataProvider().setRundown(cachedRundown.id, cachedRundown);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the customFields are mutable we persist the changes
|
// if the customFields are mutable we persist the changes
|
||||||
if (options.mutableCustomFields) {
|
if (options.mutableCustomFields) {
|
||||||
projectCustomFields = customFields;
|
// schedule a database update
|
||||||
|
setImmediate(async () => {
|
||||||
|
await getDataProvider().setCustomFields(projectCustomFields);
|
||||||
|
});
|
||||||
|
|
||||||
// persist after reassignment
|
projectCustomFields = customFields;
|
||||||
getDataProvider().setCustomFields(projectCustomFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -157,6 +163,50 @@ export function createTransaction(options: TransactionOptions): Transaction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add entry to rundown, handles the following cases:
|
||||||
|
* - 1a. add entry in group, after a given entry
|
||||||
|
* - 1b. add entry in group, at the beginning
|
||||||
|
* - 2a. add entry to the rundown, after a given entry
|
||||||
|
* - 2b. add entry to the rundown, at the beginning
|
||||||
|
*/
|
||||||
|
function add(rundown: Rundown, entry: OntimeEntry, afterId: EntryId | null, parent: OntimeGroup | null): OntimeEntry {
|
||||||
|
if (parent) {
|
||||||
|
// 1. inserting an entry inside a group
|
||||||
|
|
||||||
|
if ('parent' in entry) {
|
||||||
|
entry.parent = parent.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterId) {
|
||||||
|
const atEventsIndex = parent.entries.indexOf(afterId) + 1;
|
||||||
|
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
|
||||||
|
parent.entries = insertAtIndex(atEventsIndex, entry.id, parent.entries);
|
||||||
|
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
|
||||||
|
} else {
|
||||||
|
parent.entries = insertAtIndex(0, entry.id, parent.entries);
|
||||||
|
const atFlatIndex = rundown.flatOrder.indexOf(parent.id) + 1;
|
||||||
|
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2. inserting an entry at top level
|
||||||
|
if (afterId) {
|
||||||
|
const atOrderIndex = rundown.order.indexOf(afterId) + 1;
|
||||||
|
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
|
||||||
|
rundown.order = insertAtIndex(atOrderIndex, entry.id, rundown.order);
|
||||||
|
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
|
||||||
|
} else {
|
||||||
|
rundown.order = insertAtIndex(0, entry.id, rundown.order);
|
||||||
|
rundown.flatOrder = insertAtIndex(0, entry.id, rundown.flatOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// either way, we insert the entry into the rundown
|
||||||
|
rundown.entries[entry.id] = entry;
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a patch of changes to an existing entry
|
* Applies a patch of changes to an existing entry
|
||||||
* @returns { entry: OntimeEntry, didInvalidate: boolean } - didInvalidate indicates whether the change warrants a recalculation of the cache
|
* @returns { entry: OntimeEntry, didInvalidate: boolean } - didInvalidate indicates whether the change warrants a recalculation of the cache
|
||||||
@@ -466,7 +516,7 @@ function clone(rundown: Rundown, entry: OntimeEntry, options?: InsertOptions): O
|
|||||||
after = entry.id;
|
after = entry.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addToRundown(rundown, clonedEntry, after, parent);
|
return add(rundown, clonedEntry, after, parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,7 +583,7 @@ function ungroup(rundown: Rundown, group: OntimeGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const rundownMutation = {
|
export const rundownMutation = {
|
||||||
add: addToRundown,
|
add,
|
||||||
edit,
|
edit,
|
||||||
remove,
|
remove,
|
||||||
removeAll,
|
removeAll,
|
||||||
@@ -548,8 +598,10 @@ export const rundownMutation = {
|
|||||||
/**
|
/**
|
||||||
* Exposes a way to update a rundown which is not active
|
* Exposes a way to update a rundown which is not active
|
||||||
*/
|
*/
|
||||||
export async function updateBackgroundRundown(rundownId: string, rundown: Rundown) {
|
export function updateBackgroundRundown(rundownId: string, rundown: Rundown) {
|
||||||
await getDataProvider().setRundown(rundownId, rundown);
|
setImmediate(async () => {
|
||||||
|
await getDataProvider().setRundown(rundownId, rundown);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -646,7 +698,9 @@ export function init(initialRundown: Readonly<Rundown>, initialCustomFields: Rea
|
|||||||
rundownMetadata = metadata;
|
rundownMetadata = metadata;
|
||||||
|
|
||||||
// defer writing to the database
|
// defer writing to the database
|
||||||
getDataProvider().setRundown(cachedRundown.id, cachedRundown);
|
setImmediate(async () => {
|
||||||
|
await getDataProvider().setRundown(cachedRundown.id, cachedRundown);
|
||||||
|
});
|
||||||
|
|
||||||
return { rundown, rundownMetadata, customFields, revision: rundown.revision };
|
return { rundown, rundownMetadata, customFields, revision: rundown.revision };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,22 +17,13 @@ import {
|
|||||||
OntimeMilestone,
|
OntimeMilestone,
|
||||||
OntimeGroup,
|
OntimeGroup,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import { isObjectEmpty, generateId, getLinkedTimes, getTimeFrom, isNewLatest } from 'ontime-utils';
|
||||||
isObjectEmpty,
|
|
||||||
generateId,
|
|
||||||
getLinkedTimes,
|
|
||||||
getTimeFrom,
|
|
||||||
isNewLatest,
|
|
||||||
createDelay,
|
|
||||||
createEvent,
|
|
||||||
createGroup,
|
|
||||||
createMilestone,
|
|
||||||
} from 'ontime-utils';
|
|
||||||
|
|
||||||
|
import { delay as delayDef } from '../../models/eventsDefinition.js';
|
||||||
import { makeNewRundown } from '../../models/dataModel.js';
|
import { makeNewRundown } from '../../models/dataModel.js';
|
||||||
import type { ErrorEmitter } from '../../utils/parserUtils.js';
|
import type { ErrorEmitter } from '../../utils/parserUtils.js';
|
||||||
|
|
||||||
import { calculateDayOffset, cleanupCustomFields } from './rundown.utils.js';
|
import { calculateDayOffset, cleanupCustomFields, createGroup, createEvent, createMilestone } from './rundown.utils.js';
|
||||||
import { RundownMetadata } from './rundown.types.js';
|
import { RundownMetadata } from './rundown.types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +107,7 @@ export function parseRundown(
|
|||||||
cleanupCustomFields(newEvent.custom, parsedCustomFields);
|
cleanupCustomFields(newEvent.custom, parsedCustomFields);
|
||||||
eventIndex += 1;
|
eventIndex += 1;
|
||||||
} else if (isOntimeDelay(event)) {
|
} else if (isOntimeDelay(event)) {
|
||||||
newEvent = createDelay({ duration: event.duration, id });
|
newEvent = { ...delayDef, duration: event.duration, id };
|
||||||
} else if (isOntimeMilestone(event)) {
|
} else if (isOntimeMilestone(event)) {
|
||||||
newEvent = createMilestone({ ...event, id });
|
newEvent = createMilestone({ ...event, id });
|
||||||
cleanupCustomFields(newEvent.custom, parsedCustomFields);
|
cleanupCustomFields(newEvent.custom, parsedCustomFields);
|
||||||
@@ -143,7 +134,7 @@ export function parseRundown(
|
|||||||
cleanupCustomFields(newNestedEvent.custom, parsedCustomFields);
|
cleanupCustomFields(newNestedEvent.custom, parsedCustomFields);
|
||||||
eventIndex += 1;
|
eventIndex += 1;
|
||||||
} else if (isOntimeDelay(nestedEvent)) {
|
} else if (isOntimeDelay(nestedEvent)) {
|
||||||
newNestedEvent = createDelay({ duration: nestedEvent.duration, id: nestedEventId });
|
newNestedEvent = { ...delayDef, duration: nestedEvent.duration, id: nestedEventId };
|
||||||
newNestedEvent.parent = event.id;
|
newNestedEvent.parent = event.id;
|
||||||
} else if (isOntimeMilestone(nestedEvent)) {
|
} else if (isOntimeMilestone(nestedEvent)) {
|
||||||
newNestedEvent = createMilestone({ ...nestedEvent, id: nestedEventId });
|
newNestedEvent = createMilestone({ ...nestedEvent, id: nestedEventId });
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user