mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 09:53:48 +00:00
Compare commits
87 Commits
v3.15.0
...
fix-storage
| Author | SHA1 | Date | |
|---|---|---|---|
| e2c9ee31ad | |||
| bfd85855d4 | |||
| 20859abda9 | |||
| 1025347c08 | |||
| 43988ea12b | |||
| 722e045b20 | |||
| ad6804019b | |||
| cd40a6e55e | |||
| 08d9e24871 | |||
| 0649678dca | |||
| a00ec2d02a | |||
| 54a74ccc2a | |||
| 6f3ab274bd | |||
| 90870ecfb6 | |||
| b640e0e181 | |||
| c41fe824cf | |||
| 62c8319d70 | |||
| b1d23467a2 | |||
| 9a62daf047 | |||
| b9ab1c6fd7 | |||
| c1054711b0 | |||
| a4d4f29a37 | |||
| 2793aadea0 | |||
| e7cfb7d9d9 | |||
| 256a851c9b | |||
| 4ab9c81cb8 | |||
| 2f13d6c89e | |||
| f1f7bad25e | |||
| a8b52a48f7 | |||
| cd0999b2ab | |||
| b4c60f3f04 | |||
| 5cefad3666 | |||
| 7a6ecd8c34 | |||
| ba96ecfd91 | |||
| 7bed3757f2 | |||
| e2e755b1d2 | |||
| a77d23109d | |||
| 0eb3b8d382 | |||
| 0021185288 | |||
| 94c72ff4f6 | |||
| e4c08dc9b2 | |||
| ec74af0d62 | |||
| b6d72dd082 | |||
| 31c311daf0 | |||
| e9b3cc6090 | |||
| 543b04a097 | |||
| d39b85b6e6 | |||
| bbe107bb2b | |||
| c616240db1 | |||
| 166be66ce3 | |||
| 4180d0a337 | |||
| 78108e316c | |||
| 3bb8b70915 | |||
| 1c4f13a0ed | |||
| 3ca0abad53 | |||
| 030c8f897f | |||
| 68175cfa3b | |||
| fd8f757851 | |||
| 876d111c61 | |||
| 730cb95c04 | |||
| 3c388d4fb5 | |||
| 2e23718d73 | |||
| 89ea8c470b | |||
| 178640bfc4 | |||
| 4ed38340e0 | |||
| 69eb9a5eff | |||
| 351425127a | |||
| 95f2ba37cc | |||
| fe8bbd5003 | |||
| c6e38a2b4a | |||
| 56f18fe71c | |||
| 0b9306de82 | |||
| 3b1e6cf33d | |||
| dda219ebbe | |||
| 8bb771e5c6 | |||
| 8723bcbd33 | |||
| 0dcea4f2d7 | |||
| 696c016c90 | |||
| eed6373dbf | |||
| ac61f257c0 | |||
| 8d8625aa77 | |||
| 1e1aa4bca5 | |||
| 1f0cde6d0d | |||
| c4b66a9ed3 | |||
| 133b3aa819 | |||
| 9566fadbbb | |||
| 62570899e8 |
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
@@ -14,12 +14,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -56,12 +56,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -89,12 +89,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -73,12 +73,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ Ontime consists of 3 distinct parts
|
|||||||
The steps below will assume you have locally installed the necessary dependencies.
|
The steps below will assume you have locally installed the necessary dependencies.
|
||||||
Other dependencies will be installed as part of the setup
|
Other dependencies will be installed as part of the setup
|
||||||
|
|
||||||
- __node__ (~20)
|
- __node__ (~22)
|
||||||
- __pnpm__ (~9)
|
- __pnpm__ (~10)
|
||||||
- __docker__ (only necessary to run and build docker images)
|
- __docker__ (only necessary to run and build docker images)
|
||||||
|
|
||||||
## LOCAL DEVELOPMENT
|
## LOCAL DEVELOPMENT
|
||||||
|
|||||||
+3
-3
@@ -1,13 +1,13 @@
|
|||||||
FROM node:20-bullseye AS builder
|
FROM node:22-bullseye AS builder
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN npm install -g pnpm@9.5.0
|
RUN npm install -g pnpm@10.11.0
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
# Environment Variable to signal that we are running production
|
# Environment Variable to signal that we are running production
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.15.0",
|
"version": "3.16.1",
|
||||||
"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": "3.15.0",
|
"version": "3.16.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"@table-nav/react": "^0.0.7",
|
"@table-nav/react": "^0.0.7",
|
||||||
"@tanstack/react-query": "^5.62.7",
|
"@tanstack/react-query": "^5.62.7",
|
||||||
"@tanstack/react-query-devtools": "^5.62.7",
|
"@tanstack/react-query-devtools": "^5.62.7",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"@typescript-eslint/parser": "catalog:",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "4.5.1",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"eslint-config-prettier": "catalog:",
|
"eslint-config-prettier": "catalog:",
|
||||||
"eslint-plugin-jest": "^28.6.0",
|
"eslint-plugin-jest": "^28.6.0",
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "^5.2.11",
|
"vite": "6.3.1",
|
||||||
"vite-plugin-compression2": "^1.3.3",
|
"vite-plugin-compression2": "1.4.0",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "4.3.0",
|
||||||
"vite-tsconfig-paths": "^4.3.1",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IoChevronDown } from 'react-icons/io5';
|
import { IoChevronDown } from 'react-icons/io5';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
|
|
||||||
|
import { baseURI } from '../../../externals';
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import ParamInput from './ParamInput';
|
import ParamInput from './ParamInput';
|
||||||
@@ -17,7 +18,7 @@ interface ViewParamsSectionProps {
|
|||||||
export default function ViewParamsSection(props: ViewParamsSectionProps) {
|
export default function ViewParamsSection(props: ViewParamsSectionProps) {
|
||||||
const { title, collapsible, options } = props;
|
const { title, collapsible, options } = props;
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false });
|
const [collapsed, setCollapsed] = useLocalStorage({ key: `${baseURI}params-${title}`, defaultValue: false });
|
||||||
|
|
||||||
const handleCollapse = () => {
|
const handleCollapse = () => {
|
||||||
if (collapsible) {
|
if (collapsible) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createContext, PropsWithChildren, useCallback, useEffect, useState } from 'react';
|
import { createContext, PropsWithChildren, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { baseURI } from '../../externals';
|
||||||
import useSettings from '../hooks-query/useSettings';
|
import useSettings from '../hooks-query/useSettings';
|
||||||
|
|
||||||
interface AppContextType {
|
interface AppContextType {
|
||||||
@@ -26,7 +27,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'pending') return;
|
if (status === 'pending') return;
|
||||||
const previousEditor = sessionStorage.getItem(storageKeys.editor);
|
const previousEditor = sessionStorage.getItem(`${baseURI}${storageKeys.editor}`);
|
||||||
|
|
||||||
if (previousEditor && previousEditor === data.editorKey) {
|
if (previousEditor && previousEditor === data.editorKey) {
|
||||||
setEditorAuth(true);
|
setEditorAuth(true);
|
||||||
@@ -34,7 +35,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
|
|||||||
setEditorAuth(data.editorKey == null || data.editorKey === '');
|
setEditorAuth(data.editorKey == null || data.editorKey === '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousOperator = sessionStorage.getItem(storageKeys.operator);
|
const previousOperator = sessionStorage.getItem(`${baseURI}${storageKeys.operator}`);
|
||||||
if (previousOperator && previousOperator === data.operatorKey) {
|
if (previousOperator && previousOperator === data.operatorKey) {
|
||||||
setOperatorAuth(true);
|
setOperatorAuth(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -55,14 +56,14 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
|
|||||||
if (permission === 'editor') {
|
if (permission === 'editor') {
|
||||||
const correct = isValid(pin, data.editorKey);
|
const correct = isValid(pin, data.editorKey);
|
||||||
if (correct) {
|
if (correct) {
|
||||||
sessionStorage.setItem(storageKeys.editor, pin);
|
sessionStorage.setItem(`${baseURI}${storageKeys.editor}`, pin);
|
||||||
}
|
}
|
||||||
setEditorAuth(correct);
|
setEditorAuth(correct);
|
||||||
return correct;
|
return correct;
|
||||||
} else if (permission === 'operator') {
|
} else if (permission === 'operator') {
|
||||||
const correct = isValid(pin, data.operatorKey);
|
const correct = isValid(pin, data.operatorKey);
|
||||||
if (correct) {
|
if (correct) {
|
||||||
sessionStorage.setItem(storageKeys.operator, pin);
|
sessionStorage.setItem(`${baseURI}${storageKeys.operator}`, pin);
|
||||||
}
|
}
|
||||||
setOperatorAuth(correct);
|
setOperatorAuth(correct);
|
||||||
return correct;
|
return correct;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function useFlatRundown() {
|
|||||||
|
|
||||||
// update data whenever the revision changes
|
// update data whenever the revision changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.revision !== -1 && data.revision !== prevRevision) {
|
if (data.revision !== -1 || data.revision !== prevRevision) {
|
||||||
const flatRundown = data.order.map((id) => data.rundown[id]);
|
const flatRundown = data.order.map((id) => data.rundown[id]);
|
||||||
setFlatRunDown(flatRundown);
|
setFlatRunDown(flatRundown);
|
||||||
setPrevRevision(data.revision);
|
setPrevRevision(data.revision);
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export const projectDataPlaceholder: ProjectData = {
|
|||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
backstageInfo: '',
|
backstageInfo: '',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { baseURI } from '../../externals';
|
||||||
|
|
||||||
export enum AppMode {
|
export enum AppMode {
|
||||||
Run = 'run',
|
Run = 'run',
|
||||||
Edit = 'edit',
|
Edit = 'edit',
|
||||||
Freeze = 'freeze',
|
Freeze = 'freeze',
|
||||||
}
|
}
|
||||||
|
|
||||||
const appModeKey = 'ontime-app-mode';
|
const appModeKey = `${baseURI}ontime-app-mode`;
|
||||||
|
|
||||||
function getModeFromSession() {
|
function getModeFromSession() {
|
||||||
return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
|
return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ClientList } from 'ontime-types';
|
import { ClientList } from 'ontime-types';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { baseURI } from '../../externals';
|
||||||
|
|
||||||
interface ClientStore {
|
interface ClientStore {
|
||||||
name?: string;
|
name?: string;
|
||||||
setName: (newValue: string) => void;
|
setName: (newValue: string) => void;
|
||||||
@@ -15,7 +17,7 @@ interface ClientStore {
|
|||||||
setClients: (clients: ClientList) => void;
|
setClients: (clients: ClientList) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientNameKey = 'ontime-client-name';
|
const clientNameKey = `${baseURI}ontime-client-name`;
|
||||||
|
|
||||||
function persistNameInStorage(newValue: string) {
|
function persistNameInStorage(newValue: string) {
|
||||||
localStorage.setItem(clientNameKey, newValue);
|
localStorage.setItem(clientNameKey, newValue);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
// eslint-disable-next-line simple-import-sort/imports
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { booleanFromLocalStorage } from '../utils/localStorage';
|
import { booleanFromLocalStorage } from '../utils/localStorage';
|
||||||
|
import { baseURI } from '../../externals';
|
||||||
|
|
||||||
enum LocalEventKeys {
|
enum LocalEventKeys {
|
||||||
Mirror = 'ontime-view-mirror',
|
Mirror = 'ontime-view-mirror',
|
||||||
@@ -12,11 +14,11 @@ type ViewOptionsStore = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useViewOptionsStore = create<ViewOptionsStore>()((set) => ({
|
export const useViewOptionsStore = create<ViewOptionsStore>()((set) => ({
|
||||||
mirror: booleanFromLocalStorage(LocalEventKeys.Mirror, false),
|
mirror: booleanFromLocalStorage(`${baseURI}${LocalEventKeys.Mirror}`, false),
|
||||||
toggleMirror: (newValue?: boolean) =>
|
toggleMirror: (newValue?: boolean) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const val = typeof newValue === 'undefined' ? !state.mirror : newValue;
|
const val = typeof newValue === 'undefined' ? !state.mirror : newValue;
|
||||||
localStorage.setItem(LocalEventKeys.Mirror, String(val));
|
localStorage.setItem(`${baseURI}${LocalEventKeys.Mirror}`, String(val));
|
||||||
return { mirror: val };
|
return { mirror: val };
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
exports[`cx() > ignores falsy values 1`] = `""`;
|
exports[`cx() > ignores falsy values 1`] = `""`;
|
||||||
|
|
||||||
exports[`cx() > merges styles 1`] = `"_test_98a1e0 _another_98a1e0"`;
|
exports[`cx() > merges styles 1`] = `"_test_d5d33f _another_d5d33f"`;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
import { IoTrash } from 'react-icons/io5';
|
||||||
import { Button, Input, Textarea } from '@chakra-ui/react';
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ type ProjectCreateFormValues = {
|
|||||||
publicUrl?: string;
|
publicUrl?: string;
|
||||||
backstageInfo?: string;
|
backstageInfo?: string;
|
||||||
backstageUrl?: string;
|
backstageUrl?: string;
|
||||||
|
custom?: { title: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||||
@@ -34,6 +36,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
formState: { isSubmitting, isValid },
|
formState: { isSubmitting, isValid },
|
||||||
setFocus,
|
setFocus,
|
||||||
} = useForm<ProjectCreateFormValues>({
|
} = useForm<ProjectCreateFormValues>({
|
||||||
@@ -44,6 +47,11 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'custom',
|
||||||
|
});
|
||||||
|
|
||||||
// set focus to first field
|
// set focus to first field
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocus('title');
|
setFocus('title');
|
||||||
@@ -59,6 +67,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
...values,
|
...values,
|
||||||
filename,
|
filename,
|
||||||
});
|
});
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
await queryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -66,6 +75,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddCustom = () => {
|
||||||
|
append({ title: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section
|
<Panel.Section
|
||||||
as='form'
|
as='form'
|
||||||
@@ -151,6 +164,42 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Custom data' description='Add custom data for your project' />
|
||||||
|
<Button variant='ontime-subtle' onClick={handleAddCustom}>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</Panel.ListItem>
|
||||||
|
{fields.map((field, idx) => (
|
||||||
|
<div key={field.id} className={style.customDataItem}>
|
||||||
|
<Panel.Paragraph>{idx + 1}.</Panel.Paragraph>
|
||||||
|
<label>
|
||||||
|
Title
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={field.title}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.title` as const)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Value
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={field.value}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.value` as const)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button variant='ontime-ghosted' onClick={() => remove(idx)}>
|
||||||
|
<IoTrash />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeEvent, useEffect, useRef } from 'react';
|
import { ChangeEvent, useEffect, useRef } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoDownloadOutline, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoDownloadOutline, IoTrash } from 'react-icons/io5';
|
||||||
import { Button, Input, Textarea } from '@chakra-ui/react';
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
import { type ProjectData } from 'ontime-types';
|
import { type ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ export default function ProjectData() {
|
|||||||
formState: { isSubmitting, isValid, isDirty, errors },
|
formState: { isSubmitting, isValid, isDirty, errors },
|
||||||
setError,
|
setError,
|
||||||
watch,
|
watch,
|
||||||
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
@@ -32,6 +33,12 @@ export default function ProjectData() {
|
|||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDirtyValues: true,
|
keepDirtyValues: true,
|
||||||
},
|
},
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'custom',
|
||||||
});
|
});
|
||||||
|
|
||||||
// reset form values if data changes
|
// reset form values if data changes
|
||||||
@@ -77,6 +84,10 @@ export default function ProjectData() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddCustom = () => {
|
||||||
|
append({ title: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (formData: ProjectData) => {
|
const onSubmit = async (formData: ProjectData) => {
|
||||||
try {
|
try {
|
||||||
await postProjectData(formData);
|
await postProjectData(formData);
|
||||||
@@ -231,6 +242,69 @@ export default function ProjectData() {
|
|||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<Panel.Section style={{ marginTop: 0 }}>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Custom data' description='' />
|
||||||
|
<Button leftIcon={<IoAdd />} size='sm' variant='ontime-subtle' onClick={handleAddCustom}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Panel.ListItem>
|
||||||
|
{fields.length > 0 &&
|
||||||
|
fields.map((field, idx) => {
|
||||||
|
const rowErrors = errors.custom?.[idx] as
|
||||||
|
| {
|
||||||
|
title?: { message?: string };
|
||||||
|
value?: { message?: string };
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
return (
|
||||||
|
<div key={field.id} className={style.customDataItem}>
|
||||||
|
<div>
|
||||||
|
<div className={style.titleRow}>
|
||||||
|
<label>
|
||||||
|
Title
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
defaultValue={field.title}
|
||||||
|
placeholder='Title of your custom data'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.title`, {
|
||||||
|
required: { value: true, message: 'Field cannot be empty' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-subtle'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
onClick={() => remove(idx)}
|
||||||
|
leftIcon={<IoTrash />}
|
||||||
|
>
|
||||||
|
Delete Entry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{rowErrors?.title?.message && <Panel.Error>{rowErrors.title.message}</Panel.Error>}
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Value
|
||||||
|
<Textarea
|
||||||
|
variant='ontime-filled'
|
||||||
|
resize='none'
|
||||||
|
size='sm'
|
||||||
|
defaultValue={field.value}
|
||||||
|
autoComplete='off'
|
||||||
|
placeholder='Text of your custom data'
|
||||||
|
{...register(`custom.${idx}.value`, {
|
||||||
|
required: { value: true, message: 'Field cannot be empty' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{rowErrors?.value?.message && <Panel.Error>{rowErrors.value.message}</Panel.Error>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -57,3 +57,18 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customDataItem {
|
||||||
|
display: contents;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.titleRow{
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export default function SourcesPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitImportPreview = async (importMap: ImportMap) => {
|
const handleSubmitImportPreview = async (importMap: ImportMap) => {
|
||||||
|
setError(''); // to clear previous error
|
||||||
if (importFlow === 'excel') {
|
if (importFlow === 'excel') {
|
||||||
try {
|
try {
|
||||||
const previewData = await importRundownPreviewExcel(importMap);
|
const previewData = await importRundownPreviewExcel(importMap);
|
||||||
|
|||||||
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoTrash } from 'react-icons/io5';
|
||||||
import { Button, IconButton, Input, Select, Tooltip } from '@chakra-ui/react';
|
import { Button, IconButton, Input, Select, Tooltip } from '@chakra-ui/react';
|
||||||
@@ -28,6 +28,7 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
|
setValue,
|
||||||
formState: { errors, isValid },
|
formState: { errors, isValid },
|
||||||
} = useForm<NamedImportMap>({
|
} = useForm<NamedImportMap>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
@@ -45,6 +46,14 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState<'' | 'export' | 'import'>('');
|
const [loading, setLoading] = useState<'' | 'export' | 'import'>('');
|
||||||
|
|
||||||
|
// Set first sheet as default worksheet when 'event schedule' sheet is not there
|
||||||
|
useEffect(() => {
|
||||||
|
if (!worksheetNames || worksheetNames.length === 0) return;
|
||||||
|
if (!worksheetNames.includes(namedImportMap.Worksheet)) {
|
||||||
|
setValue('Worksheet', worksheetNames[0], { shouldValidate: true, shouldDirty: true });
|
||||||
|
}
|
||||||
|
}, [worksheetNames, setValue, namedImportMap.Worksheet]);
|
||||||
|
|
||||||
const handleExport = async (values: NamedImportMap) => {
|
const handleExport = async (values: NamedImportMap) => {
|
||||||
setLoading('export');
|
setLoading('export');
|
||||||
const importMap = convertToImportMap(values);
|
const importMap = convertToImportMap(values);
|
||||||
|
|||||||
+4
-2
@@ -1,5 +1,7 @@
|
|||||||
import { ImportCustom, ImportMap } from 'ontime-utils';
|
import { ImportCustom, ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { baseURI } from '../../../../../externals';
|
||||||
|
|
||||||
export type NamedImportMap = typeof namedImportMap;
|
export type NamedImportMap = typeof namedImportMap;
|
||||||
|
|
||||||
// Record of label and import name
|
// Record of label and import name
|
||||||
@@ -64,11 +66,11 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function persistImportMap(options: NamedImportMap) {
|
export function persistImportMap(options: NamedImportMap) {
|
||||||
localStorage.setItem('ontime-import-options', JSON.stringify(options));
|
localStorage.setItem(`${baseURI}ontime-import-options`, JSON.stringify(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPersistImportMap(): unknown {
|
function getPersistImportMap(): unknown {
|
||||||
const options = localStorage.getItem('ontime-import-options');
|
const options = localStorage.getItem(`${baseURI}ontime-import-options`);
|
||||||
if (!options) {
|
if (!options) {
|
||||||
throw new Error('no import options found');
|
throw new Error('no import options found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { MILLIS_PER_HOUR, MILLIS_PER_SECOND, parseUserTime } from 'ontime-utils'
|
|||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||||
import { setPlayback } from '../../../../common/hooks/useSocket';
|
import { setPlayback } from '../../../../common/hooks/useSocket';
|
||||||
|
import { baseURI } from '../../../../externals';
|
||||||
import { tooltipDelayMid } from '../../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../../ontimeConfig';
|
||||||
import TapButton from '../tap-button/TapButton';
|
import TapButton from '../tap-button/TapButton';
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ interface AddTimeProps {
|
|||||||
|
|
||||||
export default function AddTime(props: AddTimeProps) {
|
export default function AddTime(props: AddTimeProps) {
|
||||||
const { playback } = props;
|
const { playback } = props;
|
||||||
const [time, setTime] = useLocalStorage({ key: 'add-time', defaultValue: 300_000 }); // 5 minutes
|
const [time, setTime] = useLocalStorage({ key: `${baseURI}add-time`, defaultValue: 300_000 }); // 5 minutes
|
||||||
|
|
||||||
const handleTimeChange = (_field: string, value: string) => {
|
const handleTimeChange = (_field: string, value: string) => {
|
||||||
const newTime = parseUserTime(value);
|
const newTime = parseUserTime(value);
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
@include ellipsis-overflow;
|
@include ellipsis-overflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.offset {
|
||||||
|
color: $muted-gray;
|
||||||
|
}
|
||||||
|
|
||||||
.ahead {
|
.ahead {
|
||||||
color: $playback-ahead;
|
color: $playback-ahead;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { memo, PropsWithChildren, ReactNode, useMemo } from 'react';
|
import { memo, PropsWithChildren, ReactNode, useMemo } from 'react';
|
||||||
|
import { Playback } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
@@ -163,10 +164,10 @@ function ProgressOverview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RuntimeOverview() {
|
function RuntimeOverview() {
|
||||||
const { clock, offset } = useRuntimePlaybackOverview();
|
const { clock, offset, playback } = useRuntimePlaybackOverview();
|
||||||
|
|
||||||
const offsetText = getOffsetText(offset);
|
const offsetText = getOffsetText(offset);
|
||||||
const offsetClasses = offset === null ? undefined : offset <= 0 ? style.behind : style.ahead;
|
const offsetClasses = cx([style.offset, playback !== Playback.Stop && (offset < 0 ? style.behind : style.ahead)]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -96,11 +96,12 @@ describe('makeTable()', () => {
|
|||||||
"Is Public? (x)",
|
"Is Public? (x)",
|
||||||
"Skip?",
|
"Skip?",
|
||||||
"lighting",
|
"lighting",
|
||||||
|
"Type",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"00:00:00",
|
"00:00:00",
|
||||||
"00:00:00",
|
"00:00:00",
|
||||||
"...",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
@@ -109,6 +110,7 @@ describe('makeTable()', () => {
|
|||||||
"x",
|
"x",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
||||||
<table className={style.cuesheet} id='cuesheet' {...listeners}>
|
<table className={style.cuesheet} id='cuesheet' {...listeners}>
|
||||||
<CuesheetHeader headerGroups={headerGroups} />
|
<CuesheetHeader headerGroups={headerGroups} />
|
||||||
<CuesheetBody rowModel={rowModel} selectedRef={selectedRef} table={table} columnSizing={columnSizing} />
|
<CuesheetBody rowModel={rowModel} selectedRef={selectedRef} table={table} />
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<CuesheetTableMenu showModal={showModal} />
|
<CuesheetTableMenu showModal={showModal} />
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ function BlockRow(props: BlockRowProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paddingRows = new Array(columnCount - 1).fill(null);
|
// guard the use case where user has hidden all columns
|
||||||
|
const fillColumns = Math.min(columnCount, 1);
|
||||||
|
|
||||||
|
const paddingRows = new Array(fillColumns).fill(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={style.blockRow}>
|
<tr className={style.blockRow}>
|
||||||
|
|||||||
+14
-5
@@ -16,16 +16,24 @@ interface CuesheetBodyProps {
|
|||||||
rowModel: RowModel<OntimeRundownEntry>;
|
rowModel: RowModel<OntimeRundownEntry>;
|
||||||
selectedRef: MutableRefObject<HTMLTableRowElement | null>;
|
selectedRef: MutableRefObject<HTMLTableRowElement | null>;
|
||||||
table: Table<OntimeRundownEntry>;
|
table: Table<OntimeRundownEntry>;
|
||||||
columnSizing: Record<string, number>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetBody(props: CuesheetBodyProps) {
|
export default function CuesheetBody(props: CuesheetBodyProps) {
|
||||||
const { rowModel, selectedRef, table, columnSizing } = props;
|
const { rowModel, selectedRef, table } = props;
|
||||||
|
|
||||||
const { selectedEventId } = useSelectedEventId();
|
const { selectedEventId } = useSelectedEventId();
|
||||||
const { hideDelays, hidePast } = useCuesheetOptions();
|
const { hideDelays, hidePast } = useCuesheetOptions();
|
||||||
|
|
||||||
const getColumnCount = lazyEvaluate(() => table.getVisibleFlatColumns().length);
|
const getVisibleColumns = lazyEvaluate(() => table.getVisibleFlatColumns());
|
||||||
|
const getColumnHash = lazyEvaluate(() => {
|
||||||
|
let columnHash = '';
|
||||||
|
const columns = getVisibleColumns();
|
||||||
|
|
||||||
|
for (let i = 0; i < columns.length; i++) {
|
||||||
|
columnHash += `${columns[i].getIndex()}-${columns[i].getSize()} `;
|
||||||
|
}
|
||||||
|
return columnHash;
|
||||||
|
});
|
||||||
|
|
||||||
let eventIndex = 0;
|
let eventIndex = 0;
|
||||||
// for the first event, it will be past if there is something selected
|
// for the first event, it will be past if there is something selected
|
||||||
@@ -41,7 +49,7 @@ export default function CuesheetBody(props: CuesheetBodyProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isOntimeBlock(entry)) {
|
if (isOntimeBlock(entry)) {
|
||||||
const columnCount = getColumnCount();
|
const columnCount = getVisibleColumns().length;
|
||||||
return <BlockRow columnCount={columnCount} key={key} title={entry.title} hidePast={isPast && hidePast} />;
|
return <BlockRow columnCount={columnCount} key={key} title={entry.title} hidePast={isPast && hidePast} />;
|
||||||
}
|
}
|
||||||
if (isOntimeDelay(entry)) {
|
if (isOntimeDelay(entry)) {
|
||||||
@@ -58,6 +66,7 @@ export default function CuesheetBody(props: CuesheetBodyProps) {
|
|||||||
if (isOntimeEvent(entry)) {
|
if (isOntimeEvent(entry)) {
|
||||||
eventIndex++;
|
eventIndex++;
|
||||||
const isSelected = key === selectedEventId;
|
const isSelected = key === selectedEventId;
|
||||||
|
const columnHash = getColumnHash();
|
||||||
|
|
||||||
if (isPast && hidePast) {
|
if (isPast && hidePast) {
|
||||||
return null;
|
return null;
|
||||||
@@ -87,7 +96,7 @@ export default function CuesheetBody(props: CuesheetBodyProps) {
|
|||||||
selectedRef={isSelected ? selectedRef : undefined}
|
selectedRef={isSelected ? selectedRef : undefined}
|
||||||
rowBgColour={rowBgColour}
|
rowBgColour={rowBgColour}
|
||||||
table={table}
|
table={table}
|
||||||
columnSizing={columnSizing}
|
columnHash={columnHash}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,20 +23,20 @@ interface EventRowProps {
|
|||||||
rowBgColour?: string;
|
rowBgColour?: string;
|
||||||
table: Table<OntimeRundownEntry>;
|
table: Table<OntimeRundownEntry>;
|
||||||
/** hack to force re-rendering of the row when the column sizes change */
|
/** hack to force re-rendering of the row when the column sizes change */
|
||||||
columnSizing: Record<string, number>;
|
columnHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(EventRow, (prevProps, nextProps) => {
|
export default memo(EventRow, (prevProps, nextProps) => {
|
||||||
return (
|
return (
|
||||||
prevProps.rowId === nextProps.rowId &&
|
prevProps.rowId === nextProps.rowId &&
|
||||||
|
prevProps.event.id === nextProps.event.id &&
|
||||||
prevProps.event.revision === nextProps.event.revision &&
|
prevProps.event.revision === nextProps.event.revision &&
|
||||||
prevProps.eventIndex === nextProps.eventIndex &&
|
prevProps.eventIndex === nextProps.eventIndex &&
|
||||||
prevProps.rowIndex === nextProps.rowIndex &&
|
prevProps.rowIndex === nextProps.rowIndex &&
|
||||||
prevProps.isPast === nextProps.isPast &&
|
prevProps.isPast === nextProps.isPast &&
|
||||||
prevProps.selectedRef === nextProps.selectedRef &&
|
prevProps.selectedRef === nextProps.selectedRef &&
|
||||||
prevProps.rowBgColour === nextProps.rowBgColour &&
|
prevProps.rowBgColour === nextProps.rowBgColour &&
|
||||||
prevProps.table === nextProps.table &&
|
prevProps.columnHash === nextProps.columnHash
|
||||||
prevProps.columnSizing === nextProps.columnSizing
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ import { useLocalStorage } from '@mantine/hooks';
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import { baseURI } from '../../../externals';
|
||||||
|
|
||||||
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
|
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: `${baseURI}table-hidden`, defaultValue: {} });
|
||||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||||
key: 'table-order',
|
key: `${baseURI}table-order`,
|
||||||
defaultValue: columns.map((col) => col.id as string),
|
defaultValue: columns.map((col) => col.id as string),
|
||||||
});
|
});
|
||||||
const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} });
|
const [columnSizing, setColumnSizing] = useLocalStorage({
|
||||||
|
key: `${baseURI}table-sizes`,
|
||||||
|
defaultValue: {},
|
||||||
|
});
|
||||||
|
|
||||||
// if the columns change, we update the dataset
|
// if the columns change, we update the dataset
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type CsvHeaderKey = OntimeEntryCommonKeys | keyof CustomFields;
|
|||||||
|
|
||||||
export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
||||||
if (field === 'timeStart' || field === 'timeEnd' || field === 'duration') {
|
if (field === 'timeStart' || field === 'timeEnd' || field === 'duration') {
|
||||||
return millisToString(data as MaybeNumber);
|
return millisToString(data as MaybeNumber, { fallback: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === 'isPublic' || field === 'skip') {
|
if (field === 'isPublic' || field === 'skip') {
|
||||||
@@ -59,6 +59,7 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
|
|||||||
'isPublic',
|
'isPublic',
|
||||||
'skip',
|
'skip',
|
||||||
...customFieldKeys,
|
...customFieldKeys,
|
||||||
|
'type',
|
||||||
];
|
];
|
||||||
|
|
||||||
const fieldTitles = [
|
const fieldTitles = [
|
||||||
@@ -73,6 +74,7 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
|
|||||||
'Is Public? (x)',
|
'Is Public? (x)',
|
||||||
'Skip?',
|
'Skip?',
|
||||||
...customFieldLabels,
|
...customFieldLabels,
|
||||||
|
'Type',
|
||||||
];
|
];
|
||||||
|
|
||||||
// add header row to data
|
// add header row to data
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: min(calc(100vw - 4rem), 800px);
|
width: min(calc(100vw - 4rem), 960px);
|
||||||
|
|
||||||
|
padding-bottom: 10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info__label {
|
.info__label {
|
||||||
@@ -33,11 +35,15 @@
|
|||||||
color: var(--label-color-override, $viewer-label-color);
|
color: var(--label-color-override, $viewer-label-color);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-top: $view-element-gap;
|
margin-top: $view-element-gap;
|
||||||
white-space: pre;
|
}
|
||||||
|
|
||||||
|
.info__value {
|
||||||
|
white-space: break-spaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.info__value {
|
a.info__value {
|
||||||
color: $action-text-color;
|
color: $action-text-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $ontime-color;
|
color: $ontime-color;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
|||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
|
||||||
import BackstageInfo from './backstage-info/BackstageInfo';
|
import BackstageInfo from './backstage-info/BackstageInfo';
|
||||||
|
import CustomInfo from './custom-info/CustomInfo';
|
||||||
import PublicInfo from './public-info/PublicInfo';
|
import PublicInfo from './public-info/PublicInfo';
|
||||||
import { projectInfoOptions } from './projectInfo.options';
|
import { projectInfoOptions } from './projectInfo.options';
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ export default function ProjectInfo(props: ProjectInfoProps) {
|
|||||||
)}
|
)}
|
||||||
<BackstageInfo general={general} />
|
<BackstageInfo general={general} />
|
||||||
<PublicInfo general={general} />
|
<PublicInfo general={general} />
|
||||||
|
<CustomInfo general={general} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
|
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
|
interface CustomInfoProps {
|
||||||
|
general: ProjectData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CustomInfo(props: CustomInfoProps) {
|
||||||
|
const { general } = props;
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const showCustom = isStringBoolean(searchParams.get('showCustom'));
|
||||||
|
|
||||||
|
if (!showCustom || general.custom === undefined || general.custom.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{general.custom.map((info, idx) => {
|
||||||
|
if (!info.title || !info.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Fragment key={`${info.title}-${idx}`}>
|
||||||
|
<div className='info__label'>{info.title}</div>
|
||||||
|
<div className='info__value'>{info.value}</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,14 +9,21 @@ export const projectInfoOptions: ViewOption[] = [
|
|||||||
{
|
{
|
||||||
id: 'showBackstage',
|
id: 'showBackstage',
|
||||||
title: 'Show backstage Data',
|
title: 'Show backstage Data',
|
||||||
description: 'Weather to show fields related to the backstage views',
|
description: 'Whether to show fields related to the backstage views',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'showPublic',
|
id: 'showPublic',
|
||||||
title: 'Show Public Data',
|
title: 'Show Public Data',
|
||||||
description: 'Weather to show fields related to the public views',
|
description: 'Whether to show fields related to the public views',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'showCustom',
|
||||||
|
title: 'Show Custom Data',
|
||||||
|
description: 'Whether to show fields related to the custom data',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ export default defineConfig({
|
|||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
additionalData: `
|
additionalData: `
|
||||||
@use './src/theme/ontimeColours' as *;
|
@use '@/theme/ontimeColours' as *;
|
||||||
@use './src/theme/ontimeStyles' as *;
|
@use '@/theme/ontimeStyles' as *;
|
||||||
@use './src/theme/mixins' as *;
|
@use '@/theme/mixins' as *;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-electron",
|
"name": "ontime-electron",
|
||||||
"version": "3.15.0",
|
"version": "3.16.1",
|
||||||
"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",
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^31.2.0",
|
"electron": "36.3.1",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "26.0.12",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"eslint-config-prettier": "catalog:",
|
"eslint-config-prettier": "catalog:",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
@@ -38,9 +38,7 @@
|
|||||||
"icon": "icon.icns"
|
"icon": "icon.icns"
|
||||||
},
|
},
|
||||||
"mac": {
|
"mac": {
|
||||||
"notarize": {
|
"notarize": true,
|
||||||
"teamId": "MDAU6QK6R4"
|
|
||||||
},
|
|
||||||
"hardenedRuntime": true,
|
"hardenedRuntime": true,
|
||||||
"gatekeeperAssess": false,
|
"gatekeeperAssess": false,
|
||||||
"entitlements": "./entitlements.plist",
|
"entitlements": "./entitlements.plist",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "3.15.0",
|
"version": "3.16.1",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as dgram from 'node:dgram';
|
|||||||
|
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||||
import { isOntimeCloud } from '../externals.js';
|
import { isOntimeCloud } from '../setup/environment.js';
|
||||||
|
|
||||||
import { integrationPayloadFromPath } from './utils/parse.js';
|
import { integrationPayloadFromPath } from './utils/parse.js';
|
||||||
import type { IAdapter } from './IAdapter.js';
|
import type { IAdapter } from './IAdapter.js';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getPropertyFromPath } from 'ontime-utils';
|
|||||||
|
|
||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
import { getState, type RuntimeState } from '../../stores/runtimeState.js';
|
import { getState, type RuntimeState } from '../../stores/runtimeState.js';
|
||||||
import { isOntimeCloud } from '../../externals.js';
|
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';
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
|||||||
backstageUrl: req.body?.backstageUrl ?? '',
|
backstageUrl: req.body?.backstageUrl ?? '',
|
||||||
backstageInfo: req.body?.backstageInfo ?? '',
|
backstageInfo: req.body?.backstageInfo ?? '',
|
||||||
projectLogo: req.body?.projectLogo ?? null,
|
projectLogo: req.body?.projectLogo ?? null,
|
||||||
|
custom: req.body?.custom ?? [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ export async function loadProject(req: Request, res: Response<MessageResponse |
|
|||||||
/**
|
/**
|
||||||
* Loads the demo project
|
* Loads the demo project
|
||||||
*/
|
*/
|
||||||
export async function loadDemo(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
export async function loadDemo(_req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
const projectName = await projectService.loadDemoProject();
|
const projectName = await projectService.loadDemoProject();
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const validateNewProject = [
|
|||||||
body('backstageInfo').optional().isString().trim(),
|
body('backstageInfo').optional().isString().trim(),
|
||||||
body('projectLogo').optional().isString().trim(),
|
body('projectLogo').optional().isString().trim(),
|
||||||
body('endMessage').optional().isString().trim(),
|
body('endMessage').optional().isString().trim(),
|
||||||
|
body('custom').optional().isArray(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import type { Request, Response } from 'express';
|
|||||||
|
|
||||||
import { removeUndefined } from '../../utils/parserUtils.js';
|
import { removeUndefined } from '../../utils/parserUtils.js';
|
||||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
|
||||||
import { editCurrentProjectData } from '../../services/project-service/ProjectService.js';
|
import { editCurrentProjectData } from '../../services/project-service/ProjectService.js';
|
||||||
|
import * as projectDao from './project.dao.js';
|
||||||
|
|
||||||
export function getProjectData(_req: Request, res: Response<ProjectData>) {
|
export function getProjectData(_req: Request, res: Response<ProjectData>) {
|
||||||
res.json(getDataProvider().getProjectData());
|
res.json(projectDao.getProjectData());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postProjectData(req: Request, res: Response<ProjectData | ErrorResponse>) {
|
export async function postProjectData(req: Request, res: Response<ProjectData | ErrorResponse>) {
|
||||||
@@ -27,6 +27,7 @@ export async function postProjectData(req: Request, res: Response<ProjectData |
|
|||||||
backstageInfo: req.body?.backstageInfo,
|
backstageInfo: req.body?.backstageInfo,
|
||||||
endMessage: req.body?.endMessage,
|
endMessage: req.body?.endMessage,
|
||||||
projectLogo: req.body?.projectLogo,
|
projectLogo: req.body?.projectLogo,
|
||||||
|
custom: req.body?.custom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedData = await editCurrentProjectData(newData);
|
const updatedData = await editCurrentProjectData(newData);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ProjectData } from 'ontime-types';
|
||||||
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a copy of the stored project data
|
||||||
|
*/
|
||||||
|
export function getProjectData(): ProjectData {
|
||||||
|
return structuredClone(getDataProvider().getProjectData());
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ export const projectSanitiser = [
|
|||||||
body('backstageInfo').optional().isString().trim(),
|
body('backstageInfo').optional().isString().trim(),
|
||||||
body('endMessage').optional().isString().trim(),
|
body('endMessage').optional().isString().trim(),
|
||||||
body('projectLogo').optional({ nullable: true }).isString().trim(),
|
body('projectLogo').optional({ nullable: true }).isString().trim(),
|
||||||
|
body('custom').optional().isArray(),
|
||||||
|
body('custom.*.title').optional().isString().trim().notEmpty(),
|
||||||
|
body('custom.*.value').optional().isString().trim().notEmpty(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getNetworkInterfaces } from '../../utils/network.js';
|
|||||||
import { getTimezoneLabel } from '../../utils/time.js';
|
import { getTimezoneLabel } from '../../utils/time.js';
|
||||||
import { password, routerPrefix } from '../../externals.js';
|
import { password, routerPrefix } from '../../externals.js';
|
||||||
import { hashPassword } from '../../utils/hash.js';
|
import { hashPassword } from '../../utils/hash.js';
|
||||||
|
import { ONTIME_VERSION } from '../../ONTIME_VERSION.js';
|
||||||
|
|
||||||
const startedAt = new Date();
|
const startedAt = new Date();
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export async function getSessionStats(): Promise<SessionStats> {
|
|||||||
projectName,
|
projectName,
|
||||||
playback,
|
playback,
|
||||||
timezone: getTimezoneLabel(startedAt),
|
timezone: getTimezoneLabel(startedAt),
|
||||||
|
version: ONTIME_VERSION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getErrorMessage, obfuscate } from 'ontime-utils';
|
|||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
import { isDocker } from '../../externals.js';
|
import { isDocker } from '../../setup/environment.js';
|
||||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import * as appState from '../../services/app-state-service/AppStateService.js';
|
import * as appState from '../../services/app-state-service/AppStateService.js';
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import cookieParser from 'cookie-parser';
|
|||||||
|
|
||||||
// import utils
|
// import utils
|
||||||
import { publicDir, srcDir } from './setup/index.js';
|
import { publicDir, srcDir } from './setup/index.js';
|
||||||
import { environment, isProduction, updateRouterPrefix } from './externals.js';
|
import { environment, isProduction } from './setup/environment.js';
|
||||||
|
import { updateRouterPrefix } from './externals.js';
|
||||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||||
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
|
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { generateId, millisToString } from 'ontime-utils';
|
|||||||
import { clock } from '../services/Clock.js';
|
import { clock } from '../services/Clock.js';
|
||||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
import { consoleSubdued, consoleError } from '../utils/console.js';
|
import { consoleSubdued, consoleError } from '../utils/console.js';
|
||||||
import { isProduction } from '../externals.js';
|
import { isProduction } from '../setup/environment.js';
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
private queue: Log[];
|
private queue: Log[];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { JSONFilePreset } from 'lowdb/node';
|
|||||||
|
|
||||||
import { isPath } from '../../utils/fileManagement.js';
|
import { isPath } from '../../utils/fileManagement.js';
|
||||||
import { shouldCrashDev } from '../../utils/development.js';
|
import { shouldCrashDev } from '../../utils/development.js';
|
||||||
import { isTest } from '../../externals.js';
|
import { isTest } from '../../setup/environment.js';
|
||||||
|
|
||||||
import { safeMerge } from './DataProvider.utils.js';
|
import { safeMerge } from './DataProvider.utils.js';
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ function getData(): Readonly<DatabaseModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setProjectData(newData: Partial<ProjectData>): ReadonlyPromise<ProjectData> {
|
async function setProjectData(newData: Partial<ProjectData>): ReadonlyPromise<ProjectData> {
|
||||||
db.data.project = { ...db.data.project, ...newData };
|
db.data.project = { ...structuredClone(db.data.project), ...structuredClone(newData) }; // Performing deep copy as we're updating / merging data
|
||||||
await persist();
|
await persist();
|
||||||
return db.data.project;
|
return db.data.project;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,27 @@ import { DatabaseModel } from 'ontime-types';
|
|||||||
* Merges a partial ontime project into a given ontime project
|
* Merges a partial ontime project into a given ontime project
|
||||||
*/
|
*/
|
||||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>): DatabaseModel {
|
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>): DatabaseModel {
|
||||||
|
const deepExisting = structuredClone(existing);
|
||||||
|
const deepNewData = structuredClone(newData);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rundown = existing.rundown,
|
rundown = deepExisting.rundown,
|
||||||
project = {},
|
project = {},
|
||||||
settings = {},
|
settings = {},
|
||||||
viewSettings = {},
|
viewSettings = {},
|
||||||
urlPresets = existing.urlPresets,
|
urlPresets = deepExisting.urlPresets,
|
||||||
customFields = existing.customFields,
|
customFields = deepExisting.customFields,
|
||||||
automation = existing.automation,
|
automation = deepExisting.automation,
|
||||||
} = newData;
|
} = deepNewData;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...existing,
|
...deepExisting,
|
||||||
rundown,
|
rundown,
|
||||||
project: { ...existing.project, ...project },
|
project: { ...deepExisting.project, ...project },
|
||||||
settings: { ...existing.settings, ...settings },
|
settings: { ...deepExisting.settings, ...settings },
|
||||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
viewSettings: { ...deepExisting.viewSettings, ...viewSettings },
|
||||||
urlPresets: urlPresets ?? existing.urlPresets,
|
urlPresets: urlPresets ?? deepExisting.urlPresets,
|
||||||
customFields: customFields ?? existing.customFields,
|
customFields: customFields ?? deepExisting.customFields,
|
||||||
automation: { ...existing.automation, ...automation },
|
automation: { ...deepExisting.automation, ...automation },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ describe('safeMerge', () => {
|
|||||||
publicInfo: 'existing backstageInfo',
|
publicInfo: 'existing backstageInfo',
|
||||||
backstageInfo: 'existing backstageInfo',
|
backstageInfo: 'existing backstageInfo',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [
|
||||||
|
{
|
||||||
|
title: 'existing custom title',
|
||||||
|
value: 'existing custom value',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
@@ -62,6 +68,12 @@ describe('safeMerge', () => {
|
|||||||
project: {
|
project: {
|
||||||
title: 'new title',
|
title: 'new title',
|
||||||
publicInfo: 'new public info',
|
publicInfo: 'new public info',
|
||||||
|
custom: [
|
||||||
|
{
|
||||||
|
title: 'new custom title',
|
||||||
|
value: 'new custom value',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// @ts-expect-error -- just testing
|
// @ts-expect-error -- just testing
|
||||||
@@ -74,6 +86,12 @@ describe('safeMerge', () => {
|
|||||||
backstageUrl: 'existing backstageUrl',
|
backstageUrl: 'existing backstageUrl',
|
||||||
backstageInfo: 'existing backstageInfo',
|
backstageInfo: 'existing backstageInfo',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [
|
||||||
|
{
|
||||||
|
title: 'new custom title',
|
||||||
|
value: 'new custom value',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,6 +125,7 @@ describe('safeMerge', () => {
|
|||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
backstageInfo: '',
|
backstageInfo: '',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|||||||
import { srcFiles } from './setup/index.js';
|
import { srcFiles } from './setup/index.js';
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// resolve running environment
|
|
||||||
const env = process.env.NODE_ENV || 'production';
|
|
||||||
|
|
||||||
export const isTest = Boolean(process.env.IS_TEST);
|
|
||||||
export const environment = isTest ? 'test' : env;
|
|
||||||
export const isDocker = env === 'docker';
|
|
||||||
export const isProduction = isDocker || (env === 'production' && !isTest);
|
|
||||||
export const isOntimeCloud = Boolean(process.env.IS_CLOUD);
|
|
||||||
export const password = process.env.SESSION_PASSWORD;
|
export const password = process.env.SESSION_PASSWORD;
|
||||||
export const routerPrefix = process.env.ROUTER_PREFIX;
|
export const routerPrefix = process.env.ROUTER_PREFIX;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const dbModel: DatabaseModel = {
|
|||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
backstageInfo: '',
|
backstageInfo: '',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
backstageUrl: 'www.github.com/cpvalente/ontime',
|
backstageUrl: 'www.github.com/cpvalente/ontime',
|
||||||
backstageInfo: 'Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal',
|
backstageInfo: 'Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime
|
|||||||
import {
|
import {
|
||||||
getCurrent,
|
getCurrent,
|
||||||
getExpectedFinish,
|
getExpectedFinish,
|
||||||
getRelativeOffset,
|
|
||||||
getRuntimeOffset,
|
getRuntimeOffset,
|
||||||
getTimerPhase,
|
getTimerPhase,
|
||||||
normaliseEndTime,
|
normaliseEndTime,
|
||||||
@@ -741,8 +740,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(-50);
|
expect(absoluteOffset).toBe(-50);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('added time subtracts time offset (positive offset)', () => {
|
it('added time subtracts time offset (positive offset)', () => {
|
||||||
@@ -764,8 +763,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(-60);
|
expect(absoluteOffset).toBe(-60);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('considers running overtime (negative offset)', () => {
|
it('considers running overtime (negative offset)', () => {
|
||||||
@@ -788,8 +787,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(-10);
|
expect(absoluteOffset).toBe(-10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('paused time is delayed time (negative offset)', () => {
|
it('paused time is delayed time (negative offset)', () => {
|
||||||
@@ -813,8 +812,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(-25);
|
expect(absoluteOffset).toBe(-25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('offset doesnt exist if we havent started', () => {
|
it('offset doesnt exist if we havent started', () => {
|
||||||
@@ -851,8 +850,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(0);
|
expect(absoluteOffset).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles loaded event', () => {
|
it('handles loaded event', () => {
|
||||||
@@ -892,8 +891,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(81000000 - 79521653); // clock - timestart
|
expect(absoluteOffset).toBe(81000000 - 79521653); // clock - timestart
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
|
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
|
||||||
@@ -945,8 +944,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(0);
|
expect(absoluteOffset).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with time-to-end, offset is the overtime', () => {
|
it('with time-to-end, offset is the overtime', () => {
|
||||||
@@ -998,8 +997,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(-400000); // <--- offset is always the overtime
|
expect(absoluteOffset).toBe(-400000); // <--- offset is always the overtime
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles time-to-end started after the end time', () => {
|
it('handles time-to-end started after the end time', () => {
|
||||||
@@ -1041,9 +1040,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
const updateCurrent = getCurrent(state);
|
const updateCurrent = getCurrent(state);
|
||||||
state.timer.current = updateCurrent;
|
state.timer.current = updateCurrent;
|
||||||
const offset = getRuntimeOffset(state);
|
const { absoluteOffset } = getRuntimeOffset(state);
|
||||||
expect(millisToString(offset)).toBe('-00:16:40');
|
expect(millisToString(absoluteOffset)).toBe('-00:16:40');
|
||||||
expect(offset).toBe(81000000 - 82000000); // <-- planned end - now
|
expect(absoluteOffset).toBe(81000000 - 82000000); // <-- planned end - now
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1068,10 +1067,9 @@ describe('getRelativeOffset()', () => {
|
|||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
state.runtime.offset = getRuntimeOffset(state);
|
const { absoluteOffset, relativeOffset } = getRuntimeOffset(state);
|
||||||
expect(state.runtime.offset).toBe(0);
|
expect(absoluteOffset).toBe(0);
|
||||||
const relativeOffsetoffset = getRelativeOffset(state);
|
expect(relativeOffset).toBe(0);
|
||||||
expect(relativeOffsetoffset).toBe(0);
|
|
||||||
});
|
});
|
||||||
it('relative offset is 0 when starting after the planed time', () => {
|
it('relative offset is 0 when starting after the planed time', () => {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -1093,10 +1091,9 @@ describe('getRelativeOffset()', () => {
|
|||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
state.runtime.offset = getRuntimeOffset(state);
|
const { absoluteOffset, relativeOffset } = getRuntimeOffset(state);
|
||||||
expect(state.runtime.offset).toBe(-50);
|
expect(absoluteOffset).toBe(-50);
|
||||||
const relativeOffsetoffset = getRelativeOffset(state);
|
expect(relativeOffset).toBe(0);
|
||||||
expect(relativeOffsetoffset).toBe(0);
|
|
||||||
});
|
});
|
||||||
it('relative offset is 0 when starting before the planed time', () => {
|
it('relative offset is 0 when starting before the planed time', () => {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -1118,10 +1115,9 @@ describe('getRelativeOffset()', () => {
|
|||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
state.runtime.offset = getRuntimeOffset(state);
|
const { absoluteOffset, relativeOffset } = getRuntimeOffset(state);
|
||||||
expect(state.runtime.offset).toBe(50);
|
expect(absoluteOffset).toBe(50);
|
||||||
const relativeOffsetoffset = getRelativeOffset(state);
|
expect(relativeOffset).toBe(0);
|
||||||
expect(relativeOffsetoffset).toBe(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Low } from 'lowdb';
|
|||||||
import { JSONFile } from 'lowdb/node';
|
import { JSONFile } from 'lowdb/node';
|
||||||
|
|
||||||
import { publicFiles } from '../../setup/index.js';
|
import { publicFiles } from '../../setup/index.js';
|
||||||
import { isTest } from '../../externals.js';
|
import { isTest } from '../../setup/environment.js';
|
||||||
import { isPath } from '../../utils/fileManagement.js';
|
import { isPath } from '../../utils/fileManagement.js';
|
||||||
import { shouldCrashDev } from '../../utils/development.js';
|
import { shouldCrashDev } from '../../utils/development.js';
|
||||||
|
|
||||||
|
|||||||
@@ -314,17 +314,13 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the title of a project
|
* Patches the current project data
|
||||||
* it handles invalidating the necessary data
|
* Handles deleting the local logo if the logo has been removed
|
||||||
*/
|
*/
|
||||||
export async function editCurrentProjectData(newData: Partial<ProjectData>) {
|
export async function editCurrentProjectData(newData: Partial<ProjectData>) {
|
||||||
const currentProjectData = getDataProvider().getProjectData();
|
const currentProjectData = getDataProvider().getProjectData();
|
||||||
const updatedProjectData = await getDataProvider().setProjectData(newData);
|
const updatedProjectData = await getDataProvider().setProjectData(newData);
|
||||||
|
|
||||||
if (currentProjectData.title !== updatedProjectData.title) {
|
|
||||||
// something
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the old logo if the logo has been removed
|
// Delete the old logo if the logo has been removed
|
||||||
if (!updatedProjectData.projectLogo && currentProjectData.projectLogo) {
|
if (!updatedProjectData.projectLogo && currentProjectData.projectLogo) {
|
||||||
const filePath = join(publicDir.logoDir, currentProjectData.projectLogo);
|
const filePath = join(publicDir.logoDir, currentProjectData.projectLogo);
|
||||||
|
|||||||
@@ -116,10 +116,10 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
|
|||||||
* Positive offset is time ahead
|
* Positive offset is time ahead
|
||||||
* Negative offset is time delayed
|
* Negative offset is time delayed
|
||||||
*/
|
*/
|
||||||
export function getRuntimeOffset(state: RuntimeState): number {
|
export function getRuntimeOffset(state: RuntimeState): { absoluteOffset: number; relativeOffset: number } {
|
||||||
// nothing to calculate if there are no loaded events or if we havent started
|
// nothing to calculate if there are no loaded events or if we havent started
|
||||||
if (state.eventNow === null || state.runtime.actualStart === null) {
|
if (state.eventNow === null || state.runtime.actualStart === null) {
|
||||||
return 0;
|
return { absoluteOffset: 0, relativeOffset: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
@@ -133,18 +133,16 @@ export function getRuntimeOffset(state: RuntimeState): number {
|
|||||||
const { clock } = state;
|
const { clock } = state;
|
||||||
const { countToEnd, timeStart } = state.eventNow;
|
const { countToEnd, timeStart } = state.eventNow;
|
||||||
const { addedTime, current, startedAt } = state.timer;
|
const { addedTime, current, startedAt } = state.timer;
|
||||||
|
const { actualStart, plannedStart } = state.runtime;
|
||||||
|
|
||||||
// if we havent started, but the timer is armed
|
// if we havent started, but the timer is armed
|
||||||
// the offset is the difference to the schedule
|
// the offset is the difference to the schedule
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
return timeStart - clock;
|
return { absoluteOffset: timeStart - clock, relativeOffset: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const overtime = Math.min(current, 0);
|
const overtime = Math.min(current, 0);
|
||||||
// in time-to-end, offset is overtime
|
// in time-to-end, offset is overtime
|
||||||
if (countToEnd) {
|
|
||||||
return overtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startOffset = timeStart - startedAt;
|
const startOffset = timeStart - startedAt;
|
||||||
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
||||||
@@ -153,24 +151,20 @@ export function getRuntimeOffset(state: RuntimeState): number {
|
|||||||
// addedTime - time added by user (negative offset)
|
// addedTime - time added by user (negative offset)
|
||||||
// pausedTime - time the playback was paused (negative offset)
|
// pausedTime - time the playback was paused (negative offset)
|
||||||
// overtime - how long the timer has been over-running (negative offset)
|
// overtime - how long the timer has been over-running (negative offset)
|
||||||
return startOffset - addedTime - pausedTime + overtime;
|
const offset = startOffset - addedTime - pausedTime + overtime;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// offset between planned rundown start and actual rundown start
|
||||||
* Calculates relative offset
|
const rundownStartOffset = actualStart - plannedStart;
|
||||||
* should always be calculated after the absolute offset
|
|
||||||
*/
|
// offset offset relative to the actual rundown start
|
||||||
export function getRelativeOffset(state: RuntimeState): number {
|
const relativeOffset = offset + rundownStartOffset;
|
||||||
const { actualStart, plannedStart, offset } = state.runtime;
|
|
||||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
// in time-to-end, offset is overtime
|
||||||
DEV: {
|
if (countToEnd) {
|
||||||
// we know actualStart and plannedStart exists as long as a timer is running
|
return { absoluteOffset: overtime, relativeOffset };
|
||||||
if (actualStart === null || plannedStart === null) {
|
|
||||||
throw new Error('timerUtils.calculate: actualStart and plannedStart must be set');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const relativeStartOffset = actualStart - plannedStart;
|
|
||||||
return offset + relativeStartOffset;
|
return { absoluteOffset: offset, relativeOffset };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// resolve running environment
|
||||||
|
const env = process.env.NODE_ENV || 'production';
|
||||||
|
|
||||||
|
export const isTest = Boolean(process.env.IS_TEST);
|
||||||
|
export const environment = isTest ? 'test' : env;
|
||||||
|
export const isDocker = env === 'docker';
|
||||||
|
export const isProduction = isDocker || (env === 'production' && !isTest);
|
||||||
|
export const isOntimeCloud = Boolean(process.env.IS_CLOUD);
|
||||||
@@ -10,7 +10,7 @@ import { dirname, join } from 'path';
|
|||||||
|
|
||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
import { ensureDirectory } from '../utils/fileManagement.js';
|
import { ensureDirectory } from '../utils/fileManagement.js';
|
||||||
import { isProduction } from '../externals.js';
|
import { isProduction } from './environment.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns public path depending on OS
|
* Returns public path depending on OS
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
getCurrent,
|
getCurrent,
|
||||||
getExpectedEnd,
|
getExpectedEnd,
|
||||||
getExpectedFinish,
|
getExpectedFinish,
|
||||||
getRelativeOffset,
|
|
||||||
getRuntimeOffset,
|
getRuntimeOffset,
|
||||||
getTimerPhase,
|
getTimerPhase,
|
||||||
} from '../services/timerUtils.js';
|
} from '../services/timerUtils.js';
|
||||||
@@ -89,7 +88,7 @@ export function getState(): Readonly<RuntimeState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* clear data related to the current event, but leav in place data about the global run state
|
/* clear data related to the current event, but leave in place data about the global run state
|
||||||
* used when loading a new event but the playback is not interrupted
|
* used when loading a new event but the playback is not interrupted
|
||||||
*/
|
*/
|
||||||
export function clearEventData() {
|
export function clearEventData() {
|
||||||
@@ -214,8 +213,9 @@ export function load(
|
|||||||
const firstStart = initialData?.firstStart;
|
const firstStart = initialData?.firstStart;
|
||||||
if (firstStart === null || typeof firstStart === 'number') {
|
if (firstStart === null || typeof firstStart === 'number') {
|
||||||
runtimeState.runtime.actualStart = firstStart;
|
runtimeState.runtime.actualStart = firstStart;
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
const { absoluteOffset, relativeOffset } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.relativeOffset = getRelativeOffset(runtimeState);
|
runtimeState.runtime.offset = absoluteOffset;
|
||||||
|
runtimeState.runtime.relativeOffset = relativeOffset;
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||||
}
|
}
|
||||||
if (typeof initialData.blockStartAt === 'number') {
|
if (typeof initialData.blockStartAt === 'number') {
|
||||||
@@ -420,8 +420,9 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
runtimeState.timer.phase = getTimerPhase(runtimeState);
|
runtimeState.timer.phase = getTimerPhase(runtimeState);
|
||||||
|
|
||||||
// update offset
|
// update offset
|
||||||
state.runtime.offset = getRuntimeOffset(state);
|
const { absoluteOffset, relativeOffset } = getRuntimeOffset(runtimeState);
|
||||||
state.runtime.relativeOffset = getRelativeOffset(state);
|
runtimeState.runtime.offset = absoluteOffset;
|
||||||
|
runtimeState.runtime.relativeOffset = relativeOffset;
|
||||||
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset;
|
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -482,8 +483,9 @@ export function addTime(amount: number) {
|
|||||||
runtimeState.timer.current += amount;
|
runtimeState.timer.current += amount;
|
||||||
|
|
||||||
// update runtime delays: over - under
|
// update runtime delays: over - under
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
const { absoluteOffset, relativeOffset } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.relativeOffset = getRelativeOffset(runtimeState);
|
runtimeState.runtime.offset = absoluteOffset;
|
||||||
|
runtimeState.runtime.relativeOffset = relativeOffset;
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -528,8 +530,9 @@ export function update(): UpdateResult {
|
|||||||
runtimeState.timer.elapsed = runtimeState.timer.duration - runtimeState.timer.current;
|
runtimeState.timer.elapsed = runtimeState.timer.duration - runtimeState.timer.current;
|
||||||
|
|
||||||
// update runtime, needs up-to-date timer state
|
// update runtime, needs up-to-date timer state
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
const { absoluteOffset, relativeOffset } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.relativeOffset = getRelativeOffset(runtimeState);
|
runtimeState.runtime.offset = absoluteOffset;
|
||||||
|
runtimeState.runtime.relativeOffset = relativeOffset;
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||||
|
|
||||||
const finishedNow =
|
const finishedNow =
|
||||||
|
|||||||
@@ -838,6 +838,7 @@ describe('getCustomFieldData()', () => {
|
|||||||
lighting: 'lx',
|
lighting: 'lx',
|
||||||
sound: 'sound',
|
sound: 'sound',
|
||||||
video: 'av',
|
video: 'av',
|
||||||
|
ontime_label: 'excel label',
|
||||||
},
|
},
|
||||||
entryId: 'id',
|
entryId: 'id',
|
||||||
} as ImportMap;
|
} as ImportMap;
|
||||||
@@ -845,6 +846,7 @@ describe('getCustomFieldData()', () => {
|
|||||||
const customFields: CustomFields = {
|
const customFields: CustomFields = {
|
||||||
lighting: { label: 'lx', type: 'string', colour: 'red' },
|
lighting: { label: 'lx', type: 'string', colour: 'red' },
|
||||||
sound: { label: 'sound', type: 'string', colour: 'green' },
|
sound: { label: 'sound', type: 'string', colour: 'green' },
|
||||||
|
ontime_key: { label: 'ontime_label', type: 'string', colour: 'blue' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = getCustomFieldData(importMap, customFields);
|
const result = getCustomFieldData(importMap, customFields);
|
||||||
@@ -864,6 +866,11 @@ describe('getCustomFieldData()', () => {
|
|||||||
colour: '',
|
colour: '',
|
||||||
label: 'video',
|
label: 'video',
|
||||||
},
|
},
|
||||||
|
ontime_key: {
|
||||||
|
type: 'string',
|
||||||
|
colour: 'blue',
|
||||||
|
label: 'ontime_label',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// it is an inverted record of <importKey, ontimeKey>
|
// it is an inverted record of <importKey, ontimeKey>
|
||||||
@@ -871,6 +878,7 @@ describe('getCustomFieldData()', () => {
|
|||||||
lx: 'lighting',
|
lx: 'lighting',
|
||||||
sound: 'sound',
|
sound: 'sound',
|
||||||
av: 'video',
|
av: 'video',
|
||||||
|
'excel label': 'ontime_key',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1022,8 +1030,8 @@ describe('parseExcel()', () => {
|
|||||||
user2: { type: 'string', colour: 'blue', label: 'user2' },
|
user2: { type: 'string', colour: 'blue', label: 'user2' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsedData = parseExcel(testdata, existingCustomFields, importMap);
|
const { customFields, rundown } = parseExcel(testdata, existingCustomFields, importMap);
|
||||||
expect(parsedData.customFields).toStrictEqual({
|
expect(customFields).toStrictEqual({
|
||||||
user0: {
|
user0: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
@@ -1075,9 +1083,9 @@ describe('parseExcel()', () => {
|
|||||||
label: 'user9',
|
label: 'user9',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(parsedData.rundown.length).toBe(2);
|
expect(rundown.length).toBe(2);
|
||||||
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
expect(rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
||||||
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
expect(rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses a file without custom fields', () => {
|
it('parses a file without custom fields', () => {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ describe('parseProject()', () => {
|
|||||||
publicInfo: 'publicInfo',
|
publicInfo: 'publicInfo',
|
||||||
backstageUrl: 'backstageUrl',
|
backstageUrl: 'backstageUrl',
|
||||||
backstageInfo: 'backstageInfo',
|
backstageInfo: 'backstageInfo',
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errorEmitter,
|
errorEmitter,
|
||||||
@@ -77,6 +78,7 @@ describe('parseProject()', () => {
|
|||||||
backstageUrl: 'backstageUrl',
|
backstageUrl: 'backstageUrl',
|
||||||
backstageInfo: 'backstageInfo',
|
backstageInfo: 'backstageInfo',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
});
|
});
|
||||||
expect(errorEmitter).not.toHaveBeenCalled();
|
expect(errorEmitter).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isProduction } from '../externals.js';
|
import { isProduction } from '../setup/environment.js';
|
||||||
|
|
||||||
import { consoleError } from './console.js';
|
import { consoleError } from './console.js';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Server } from 'http';
|
|||||||
import { networkInterfaces } from 'os';
|
import { networkInterfaces } from 'os';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
|
|
||||||
import { isDocker, isOntimeCloud, isProduction } from '../externals.js';
|
import { isDocker, isOntimeCloud, isProduction } from '../setup/environment.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
customFieldLabelToKey,
|
customFieldLabelToKey,
|
||||||
|
customKeyFromLabel,
|
||||||
defaultImportMap,
|
defaultImportMap,
|
||||||
generateId,
|
generateId,
|
||||||
type ImportMap,
|
type ImportMap,
|
||||||
@@ -60,7 +61,7 @@ export function getCustomFieldData(
|
|||||||
const customFields = {};
|
const customFields = {};
|
||||||
const customFieldImportKeys = {};
|
const customFieldImportKeys = {};
|
||||||
for (const ontimeLabel in importMap.custom) {
|
for (const ontimeLabel in importMap.custom) {
|
||||||
const ontimeKey = customFieldLabelToKey(ontimeLabel);
|
const ontimeKey = customKeyFromLabel(ontimeLabel, existingCustomFields) ?? customFieldLabelToKey(ontimeLabel);
|
||||||
const importLabel = importMap.custom[ontimeLabel].toLowerCase();
|
const importLabel = importMap.custom[ontimeLabel].toLowerCase();
|
||||||
const colour = ontimeKey in existingCustomFields ? existingCustomFields[ontimeKey].colour : '';
|
const colour = ontimeKey in existingCustomFields ? existingCustomFields[ontimeKey].colour : '';
|
||||||
customFields[ontimeKey] = {
|
customFields[ontimeKey] = {
|
||||||
@@ -198,9 +199,9 @@ export const parseExcel = (
|
|||||||
entryIdIndex = col;
|
entryIdIndex = col;
|
||||||
rundownMetadata['id'] = { row, col };
|
rundownMetadata['id'] = { row, col };
|
||||||
},
|
},
|
||||||
custom: (row: number, col: number, columnText: string) => {
|
custom: (row: number, col: number, columnText: string, ontimeKey: string) => {
|
||||||
customFieldIndexes[col] = columnText;
|
customFieldIndexes[col] = columnText;
|
||||||
rundownMetadata[`custom:${columnText}`] = { row, col };
|
rundownMetadata[`custom:${ontimeKey}`] = { row, col };
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -214,7 +215,7 @@ export const parseExcel = (
|
|||||||
const maybeTimeType = makeString(column, '');
|
const maybeTimeType = makeString(column, '');
|
||||||
if (maybeTimeType === 'block') {
|
if (maybeTimeType === 'block') {
|
||||||
event.type = SupportedEvent.Block;
|
event.type = SupportedEvent.Block;
|
||||||
} else if (maybeTimeType === '' || isKnownTimerType(maybeTimeType)) {
|
} else if (maybeTimeType === '' || maybeTimeType === 'event' || isKnownTimerType(maybeTimeType)) {
|
||||||
event.type = SupportedEvent.Event;
|
event.type = SupportedEvent.Event;
|
||||||
event.timerType = validateTimerType(maybeTimeType);
|
event.timerType = validateTimerType(maybeTimeType);
|
||||||
} else {
|
} else {
|
||||||
@@ -266,12 +267,13 @@ export const parseExcel = (
|
|||||||
|
|
||||||
// check if it is an ontime column
|
// check if it is an ontime column
|
||||||
if (handlers[columnText]) {
|
if (handlers[columnText]) {
|
||||||
handlers[columnText](rowIndex, j, undefined);
|
handlers[columnText](rowIndex, j, undefined, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if it is a custom field
|
// check if it is a custom field
|
||||||
if (columnText in customFieldImportKeys) {
|
if (columnText in customFieldImportKeys) {
|
||||||
handlers.custom(rowIndex, j, columnText);
|
const ontimeKey = customFieldImportKeys[columnText];
|
||||||
|
handlers.custom(rowIndex, j, columnText, ontimeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// else. we don't know how to handle this column
|
// else. we don't know how to handle this column
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export function parseProject(data: Partial<DatabaseModel>, emitError?: ErrorEmit
|
|||||||
backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl,
|
backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl,
|
||||||
backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo,
|
backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo,
|
||||||
projectLogo: data.project.projectLogo ?? dbModel.project.projectLogo,
|
projectLogo: data.project.projectLogo ?? dbModel.project.projectLogo,
|
||||||
|
custom: data.project.custom ?? dbModel.project.custom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -410,7 +410,8 @@
|
|||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
||||||
"projectLogo": null
|
"projectLogo": null,
|
||||||
|
"custom": []
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"app": "ontime",
|
"app": "ontime",
|
||||||
@@ -455,4 +456,4 @@
|
|||||||
"label": "artist"
|
"label": "artist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-2
@@ -410,7 +410,8 @@
|
|||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
||||||
"projectLogo": null
|
"projectLogo": null,
|
||||||
|
"custom": []
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"app": "ontime",
|
"app": "ontime",
|
||||||
@@ -455,4 +456,4 @@
|
|||||||
"label": "artist"
|
"label": "artist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.15.0",
|
"version": "3.16.1",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ontime",
|
"ontime",
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
},
|
},
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~20",
|
"node": "~22",
|
||||||
"pnpm": "~9"
|
"pnpm": "~10"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -50,5 +50,5 @@
|
|||||||
"turbo": "^2.3.3",
|
"turbo": "^2.3.3",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.0"
|
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface SessionStats {
|
|||||||
projectName: string;
|
projectName: string;
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetInfo {
|
export interface GetInfo {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export type ProjectData = {
|
|||||||
backstageUrl: string;
|
backstageUrl: string;
|
||||||
backstageInfo: string;
|
backstageInfo: string;
|
||||||
projectLogo: string | null;
|
projectLogo: string | null;
|
||||||
|
custom: { title: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export { isAlphanumeric, isAlphanumericWithSpace } from './src/regex-utils/isAlp
|
|||||||
export { isColourHex } from './src/regex-utils/isColourHex.js';
|
export { isColourHex } from './src/regex-utils/isColourHex.js';
|
||||||
export { splitWhitespace } from './src/regex-utils/splitWhitespace.js';
|
export { splitWhitespace } from './src/regex-utils/splitWhitespace.js';
|
||||||
|
|
||||||
export { customFieldLabelToKey } from './src/customField-utils/customFieldLabelToKey.js';
|
export { customFieldLabelToKey, customKeyFromLabel } from './src/customField-utils/customFieldLabelToKey.js';
|
||||||
|
|
||||||
// helpers from externals
|
// helpers from externals
|
||||||
export { deepmerge } from './src/externals/deepmerge.js';
|
export { deepmerge } from './src/externals/deepmerge.js';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { isAlphanumericWithSpace } from '../regex-utils/isAlphanumeric.js';
|
import { isAlphanumericWithSpace } from '../regex-utils/isAlphanumeric.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,3 +11,11 @@ export const customFieldLabelToKey = (label: string): string | null => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const customKeyFromLabel = (label: string, fields: CustomFields): string | null => {
|
||||||
|
const maybeMatchingKey = Object.keys(fields).find((key) => fields[key].label === label);
|
||||||
|
if (maybeMatchingKey) {
|
||||||
|
return maybeMatchingKey;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
Generated
+1834
-635
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -1,14 +1,16 @@
|
|||||||
# https://pnpm.io/pnpm-workspace_yaml
|
|
||||||
packages:
|
packages:
|
||||||
- "apps/*"
|
- apps/*
|
||||||
- "packages/*"
|
- packages/*
|
||||||
catalog:
|
catalog:
|
||||||
typescript: 5.5.3
|
typescript: 5.5.3
|
||||||
"@typescript-eslint/eslint-plugin": 7.16.1
|
'@typescript-eslint/eslint-plugin': 7.16.1
|
||||||
"@typescript-eslint/parser": 7.16.1
|
'@typescript-eslint/parser': 7.16.1
|
||||||
"@types/node": 20.17.16
|
'@types/node': 22.15.26
|
||||||
prettier: 3.3.1
|
prettier: 3.3.1
|
||||||
eslint: 8.56.0
|
eslint: 8.56.0
|
||||||
eslint-config-prettier: 9.1.0
|
eslint-config-prettier: 9.1.0
|
||||||
eslint-plugin-prettier: 5.1.3
|
eslint-plugin-prettier: 5.1.3
|
||||||
vitest: 2.1.6
|
vitest: 3.2.1
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- electron
|
||||||
|
- esbuild
|
||||||
|
|||||||
Reference in New Issue
Block a user