Compare commits

..

28 Commits

Author SHA1 Message Date
cv 1b867b0dbc chore: update tests 2023-09-29 22:23:16 +02:00
cv 34b79e7699 chore: update tests 2023-09-29 22:21:15 +02:00
cv 4498a04a34 refactor: align import map to demo 2023-09-29 22:16:39 +02:00
cv d9df866308 refactor: enforce integers in numbers 2023-09-29 22:10:40 +02:00
cv 8146d88765 refactor: force refetch after mutation 2023-09-29 21:56:15 +02:00
cv d5cb2735ec Merge remote-tracking branch 'origin/master' into excel-import 2023-09-29 21:53:13 +02:00
cv 9b12afe2a6 refactor: align import map to demo 2023-09-29 21:40:22 +02:00
cv e10c0e8c97 refactor: typescript improvements 2023-09-29 21:39:50 +02:00
cv 4e9d9fc075 refactor: parse time fields 2023-09-29 21:12:40 +02:00
cv 45ece13d04 refactor: g-sheet time is object 2023-09-29 21:12:27 +02:00
cv e5e5798272 refactor: update type 2023-09-29 21:12:01 +02:00
cv cf7f6bdbf7 feat: resolve times on excel import, refs #508 2023-09-29 20:19:29 +02:00
cv a0d4f40bec refactor: small cleanups and type improvement 2023-09-28 08:15:54 +02:00
cv ba5ef6668d refactor: parse import fields 2023-09-28 08:15:24 +02:00
cv 9fc04f2fd1 chore: create feature endpoints 2023-09-27 15:09:48 +02:00
cv 13d72dd1a4 style: small presentation tweaks 2023-09-27 15:08:14 +02:00
cv 30905757f3 chore: increase size limits on uploads 2023-09-27 14:31:14 +02:00
cv e09a7d99f0 refactor: handle unexpected data types 2023-09-27 12:16:39 +02:00
cv 52ca04e063 chore: create import map utilities 2023-09-27 12:15:50 +02:00
cv b8188c7485 chore: add external deepmerge utility 2023-09-27 12:14:04 +02:00
cv 85172ae8d7 temp: handle inconsistent errors from server 2023-09-27 09:56:24 +02:00
cv cb871a8c26 refactor: simplify handling of notification 2023-09-27 09:55:18 +02:00
cv 88a853c158 refactor: convert to typescript 2023-09-17 20:55:23 +02:00
cv 99cd9bf0b7 refactor: simplify handling axios errors 2023-09-17 20:49:02 +02:00
cv 29fd145bd9 wip: create upload UI components 2023-09-17 20:48:27 +02:00
cv f6440a503f chore: upgrade deps 2023-09-17 13:16:46 +02:00
cv aa0d4103b1 style: tweaks on components 2023-09-16 14:09:49 +02:00
cv 12b051dadb refactor: rename utilities file 2023-09-16 14:09:17 +02:00
140 changed files with 2857 additions and 4466 deletions
-1
View File
@@ -1 +0,0 @@
"ONTIME_VERSION.js"
+2 -38
View File
@@ -1,5 +1,4 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": 2020
},
@@ -7,16 +6,8 @@
"es6": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"eslint-config-prettier"
],
"plugins": [
"@typescript-eslint",
"prettier"
"eslint:recommended"
],
"overrides": [
{
@@ -30,33 +21,6 @@
}
],
"rules": {
"no-useless-concat": "warn",
"prefer-template": "warn",
"no-console": [
"warn",
{
"allow": [
"warn",
"error"
]
}
],
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"prettier/prettier": [
"warn",
{
"endOfLine": "auto"
}
]
"no-console": "warn"
}
}
+6 -6
View File
@@ -16,17 +16,17 @@ jobs:
- uses: actions/checkout@v3
- name: Setup env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Setup Node.js environment
uses: actions/setup-node@v3.6.0
with:
version: 16.16.0
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -34,7 +34,7 @@ jobs:
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm turbo build:docker
- name: Docker Login
uses: docker/login-action@v2.1.0
with:
+6 -6
View File
@@ -17,9 +17,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -52,9 +52,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -85,9 +85,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
+4 -28
View File
@@ -20,47 +20,23 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Run code quality per package
- name: React - Run linter
if: always()
run: pnpm lint
working-directory: ./apps/client
- name: Server - Run linter
if: always()
run: pnpm lint
working-directory: ./apps/server
- name: Utils - Run linter
if: always()
run: pnpm lint
working-directory: ./packages/utils
- name: Types - Run linter
if: always()
run: pnpm lint
working-directory: ./packages/types
# We choose to run tests separately
- name: React - Run unit tests
if: always()
run: pnpm test:pipeline
working-directory: ./apps/client
- name: Server - Run unit tests
if: always()
run: pnpm test:pipeline
working-directory: ./apps/server
- name: Utils - Run unit tests
if: always()
run: pnpm test:pipeline
working-directory: ./packages/utils
@@ -76,9 +52,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint
BIN
View File
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
build
coverage
dist
node_modules
playwright-report
**/*.toml
**/*.yml
**/*.json
-1
View File
@@ -1,5 +1,4 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
-2
View File
@@ -56,8 +56,6 @@ E2E tests are in a separate package. On running, [playwright](https://playwright
webserver to test against
These tests also run against a separate version of the DB (test-db)
Before running the E2E, you should first build the project with `pnpm build:local`.
You can run playwright tests from project root with `pnpm e2e`
When writing tests, it can be handy to run playwright in interactive mode with `pnpm e2e:i`. You would need to manually
+44 -36
View File
@@ -14,11 +14,10 @@
Ontime is an application for creating and managing event running order and timers.
The user inputs a list of events along with scheduling and event information.
The user inputs a list of events along with scheduling and event information.
This will then populate a series of screens which are available to be rendered by any device in the Network.
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video
outputs.
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video outputs.
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/app.png)
@@ -29,7 +28,7 @@ outputs.
Once installed and running, Ontime starts a background server that is the heart of all processes.
From the app, you can add / edit your running order and control the timer playback.
Any device with a browser in the same network can choose one of the supported views to render the available data.
Any device with a browser in the same network can choose one of the supported views to render the available data.
This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001`
or `192.168.1.3:4001`
<br />
@@ -51,9 +50,8 @@ IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
```
```
For management views
For management views
-------------------------------------------------------------
IP.ADDRESS:4001/editor > the control interface, same as the app
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
@@ -65,14 +63,14 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
- [x] Distribute data over network and render it in the browser
- [x] Different screen types
- Stage Timer
- Minimal Timer
- Clock
- Backstage Info
- Public Info
- Studio Clock
- Countdown
- [Make your own?](#make-your-own-viewer)
- Stage Timer
- Minimal Timer
- Clock
- Backstage Info
- Public Info
- Studio Clock
- Countdown
- [Make your own?](#make-your-own-viewer)
- [x] Configurable Lower Thirds
- [x] Collaborative editing with the cuesheet view
- [x] Send live messages to different screen types
@@ -85,19 +83,17 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
- [x] Roll mode: run standalone using the system clock
- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally
- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have
a countdown to any scheduled event
- [x] Multi-platform (available on Windows, MacOS and Linux)
a countdown to any scheduled event
- [x] Multi-platform (available on Windows, MacOS and Linux)
- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime)
## Unopinionated
We want Ontime to be unique by targeting freelancers instead of roles.
We want Ontime to be unique by targeting freelancers instead of roles.
We believe most freelancers work in different fields and we want to give you a tool that you can leverage across your
many environments and workflows.
We believe most freelancers work in different fields and we want to give you a tool that you can leverage across your many environments and workflows.
We are not interested in forcing workflows and have made Ontime so, it is flexible to whichever way you would like to
work.
We are not interested in forcing workflows and have made Ontime so, it is flexible to whichever way you would like to work.
## Rich APIs for workflow integrations
@@ -117,8 +113,7 @@ Taking advantage of the integrations, we currently use Ontime with:
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside the application.
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language
that can run in the browser).
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
<br />
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
how to get you started and read the docs about
@@ -126,11 +121,26 @@ the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/ontime-api
### Headless run
You can self-host and run Ontime in a docker image.
You can self-host and run Ontime in a docker image. The run command will:
The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
- expose the necessary ports (listed in the Dockerfile)
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
- the image name __getontime/ontime__
If you want to run this image in a Raspberry Pi, please see [the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
The docker image is
in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
```bash
docker pull getontime/ontime
```
and use the included docker compose to get started
```bash
docker-compose up
```
Related information available [in the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
## Roadmap
@@ -138,32 +148,32 @@ If you want to run this image in a Raspberry Pi, please see [the docs](https://o
Several features are planned in the roadmap, and we continuously adjust this to match how users interact with the app.
<br />
Have an idea? Reach out via [email](mail@getontime.no)
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
Have an idea? Reach out via [email](mail@getontime.no) or [open an issue](https://github.com/cpvalente/ontime/issues/new)
### Issues
We use Github's issue tracking for bug reporting and feature requests. <br />
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
#### Unsigned App
When installing the app you would see warning screens from the Operating System like:
`Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.`
```Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.```
or
`Ontime can't be opened because it is from an unidentified developer`
```Ontime can't be opened because it is from an unidentified developer```
or in Linux
`Could Not Display "ontime-linux.AppImage`
```Could Not Display "ontime-linux.AppImage```
You can circumvent this by allowing the execution of the app manually.
- In Windows: click more and select "Run Anyway"
- in macOS: the solution in macOS is different across versions, please refer to the [apple documentation](https://support.apple.com/en-gb/guide/mac-help/mh40616/mac)
- in macOS: after attempting to run the installer, navigate to System Preferences -> Security &
Privacy and allow the execution of the app
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
File as a Program
@@ -175,7 +185,6 @@ please [open an issue](https://github.com/cpvalente/ontime/issues/new)
#### Safari
There are known issues with Safari versions lower than 13:
- Spacing and text styles might have small inconsistencies
- Table view does not work
@@ -185,8 +194,7 @@ There is no plan for any further work on this.
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
<br /><br />
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a
Pull Request.
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a Pull Request.
<br />
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
+18 -2
View File
@@ -8,18 +8,34 @@
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier",
"plugin:@tanstack/eslint-plugin-query/recommended",
"prettier"
],
"plugins": [
"react",
"testing-library",
"simple-import-sort",
"@tanstack/query"
"@tanstack/query",
"@typescript-eslint",
"prettier"
],
"rules": {
"@typescript-eslint/no-non-null-assertion": "warn",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"no-useless-concat": "warn",
"prefer-template": "warn",
"react/jsx-no-bind": [
"error",
{
-1
View File
@@ -1,5 +1,4 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
+11 -10
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "2.16.2",
"version": "2.9.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.7.0",
@@ -39,7 +39,7 @@
"build": "vite build",
"build:local": "cross-env NODE_ENV=local vite build",
"build:docker": "vite build",
"lint": "eslint . --quiet",
"lint": "eslint .",
"test": "vitest",
"test:pipeline": "vitest run",
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build"
@@ -63,16 +63,17 @@
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.1.1",
"@types/color": "^3.0.3",
"@types/luxon": "^3.2.0",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"@vitejs/plugin-react": "^3.0.1",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jest": "^27.1.7",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
@@ -80,9 +81,9 @@
"jsdom": "^21.1.0",
"ontime-types": "workspace:*",
"ontime-utils": "workspace:*",
"prettier": "^3.0.3",
"prettier": "^2.8.3",
"sass": "^1.57.1",
"typescript": "^5.2.2",
"typescript": "^4.9.4",
"vite": "^4.3.1",
"vite-plugin-compression2": "^0.9.0",
"vite-plugin-svgr": "^2.4.0",
+12 -15
View File
@@ -11,6 +11,7 @@ export function maybeAxiosError(error: unknown) {
const statusText = (error as AxiosError).response?.statusText ?? '';
let data = (error as AxiosError).response?.data ?? '';
if (typeof data === 'object') {
// TODO: use error instead, when migrated
if ('message' in data) {
data = JSON.stringify(data.message);
} else {
@@ -19,10 +20,7 @@ export function maybeAxiosError(error: unknown) {
}
return `${statusText}: ${data}`;
} else {
if (typeof error !== 'string') {
return JSON.stringify(error);
}
return error;
return error as string;
}
}
@@ -38,16 +36,15 @@ export function logAxiosError(prepend: string, error: unknown) {
});
}
/**
* Utility function invalidates react-query caches
*/
export async function invalidateAllCaches() {
await ontimeQueryClient.invalidateQueries(['project']);
await ontimeQueryClient.invalidateQueries(['aliases']);
await ontimeQueryClient.invalidateQueries(['userFields']);
await ontimeQueryClient.invalidateQueries(['rundown']);
await ontimeQueryClient.invalidateQueries(['appinfo']);
await ontimeQueryClient.invalidateQueries(['oscSettings']);
await ontimeQueryClient.invalidateQueries(['appSettings']);
await ontimeQueryClient.invalidateQueries(['viewSettings']);
await ontimeQueryClient.invalidateQueries([
'project',
'aliases',
'userFields',
'rundown',
'appinfo',
'oscSettings',
'appSettings',
'viewSettings',
]);
}
+27 -12
View File
@@ -2,7 +2,6 @@ import axios, { AxiosResponse } from 'axios';
import {
Alias,
DatabaseModel,
GetInfo,
OntimeRundown,
OSCSettings,
OscSubscription,
@@ -14,7 +13,7 @@ import {
import { ExcelImportMap } from 'ontime-utils';
import { apiRepoLatest } from '../../externals';
import fileDownload from '../utils/fileDownload';
import { InfoType } from '../models/Info';
import { ontimeURL } from './apiConstants';
@@ -39,7 +38,7 @@ export async function postSettings(data: Settings) {
* @description HTTP request to retrieve application info
* @return {Promise}
*/
export async function getInfo(): Promise<GetInfo> {
export async function getInfo(): Promise<InfoType> {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
}
@@ -121,17 +120,32 @@ export async function postOscSubscriptions(data: OscSubscription) {
}
/**
* @description HTTP request to download db in CSV format
* @description HTTP request to download db
* @return {Promise}
*/
export const downloadCSV = () => {
return fileDownload(ontimeURL, { name: 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' });
};
export const downloadRundown = async () => {
await axios({
url: `${ontimeURL}/db`,
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const headerLine = response.headers['Content-Disposition'];
let filename = 'rundown.json';
/**
* @description HTTP request to download db in JSON format
*/
export const downloadRundown = () => {
return fileDownload(ontimeURL, { name: 'rundown', type: 'json' }, { type: 'application/json;charset=utf-8;' });
// try and get the filename from the response
if (headerLine != null) {
const startFileNameIndex = headerLine.indexOf('"') + 1;
const endFileNameIndex = headerLine.lastIndexOf('"');
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
});
};
// TODO: should this be extracted to shared code?
@@ -152,6 +166,7 @@ export const uploadProjectFile = async (
formData.append('userFile', file);
const onlyRundown = Boolean(options?.onlyRundown);
console.log('debug here', onlyRundown, options);
await axios
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
@@ -25,14 +25,8 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
return (
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
<div
className='multiprogress-bar__bg-warning'
style={{ width: `${warningWidth}%`, backgroundColor: warningColor }}
/>
<div
className='multiprogress-bar__bg-danger'
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
/>
<div className='multiprogress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
<div className='multiprogress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
</div>
);
@@ -1,3 +1,5 @@
import Empty from '../state/Empty';
import { useSchedule } from './ScheduleContext';
import ScheduleItem from './ScheduleItem';
@@ -11,9 +13,8 @@ interface ScheduleProps {
export default function Schedule({ isProduction, className }: ScheduleProps) {
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
// TODO: design a nice placeholder for empty schedules
if (paginatedEvents?.length < 1) {
return null;
return <Empty text='No events to show' />;
}
let selectedState: 'past' | 'now' | 'future' = 'past';
@@ -1,9 +1,7 @@
import { createContext, PropsWithChildren, useContext, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { OntimeEvent } from 'ontime-types';
import { useInterval } from '../../hooks/useInterval';
import { isStringBoolean } from '../../utils/viewUtils';
interface ScheduleContextState {
events: OntimeEvent[];
@@ -21,6 +19,7 @@ interface ScheduleProviderProps {
events: OntimeEvent[];
selectedEventId: string | null;
isBackstage?: boolean;
eventsPerPage?: number;
time?: number;
}
@@ -29,29 +28,16 @@ export const ScheduleProvider = ({
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 7,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
const [visiblePage, setVisiblePage] = useState(0);
const [searchParams] = useSearchParams();
// look for overrides from views
const hidePast = isStringBoolean(searchParams.get('hidePast'));
const stopCycle = isStringBoolean(searchParams.get('stopCycle'));
const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? 7);
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
const viewEvents = [...events];
if (hidePast) {
// we want to show the event after the next
viewEvents.splice(0, selectedEventIndex + 2);
selectedEventIndex = 0;
}
const numPages = Math.ceil(viewEvents.length / eventsPerPage);
const numPages = Math.ceil(events.length / eventsPerPage);
const eventStart = eventsPerPage * visiblePage;
const eventEnd = eventsPerPage * (visiblePage + 1);
const paginatedEvents = viewEvents.slice(eventStart, eventEnd);
const paginatedEvents = events.slice(eventStart, eventEnd);
const selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
const resolveScheduleType = () => {
if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) {
@@ -66,9 +52,7 @@ export const ScheduleProvider = ({
// every SCROLL_TIME go to the next array
useInterval(() => {
if (stopCycle) {
setVisiblePage(0);
} else if (events.length > eventsPerPage) {
if (events.length > eventsPerPage) {
const next = (visiblePage + 1) % numPages;
setVisiblePage(next);
}
@@ -14,7 +14,16 @@ interface ScheduleItemProps {
}
export default function ScheduleItem(props: ScheduleItemProps) {
const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props;
const {
selected,
timeStart,
timeEnd,
title,
presenter,
backstageEvent,
colour,
skip,
} = props;
const start = formatTime(timeStart, { format: 'hh:mm' });
const end = formatTime(timeEnd, { format: 'hh:mm' });
@@ -28,7 +37,9 @@ export default function ScheduleItem(props: ScheduleItemProps) {
{`${start}${end} ${backstageEvent ? '*' : ''}`}
</div>
<div className='entry-title'>{title}</div>
{presenter && <div className='entry-presenter'>{presenter}</div>}
{presenter && (
<div className='entry-presenter'>{presenter}</div>
)}
</li>
);
}
@@ -13,11 +13,12 @@ export default function ScheduleNav({ className }: ScheduleNavProps) {
<div className={`schedule-nav ${className}`}>
{numPages > 1 &&
[...Array(numPages).keys()].map((i) => (
<div
key={i}
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
/>
))}
<div
key={i}
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
/>
),
)}
</div>
);
}
@@ -38,6 +38,12 @@
gap: $element-inner-spacing;
}
.noHover {
&:hover {
background-color: inherit;
}
}
.title {
font-size: $inner-section-text-size;
display: block;
@@ -1,5 +1,5 @@
import { FormEvent, useEffect } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import {
Button,
Drawer,
@@ -12,27 +12,11 @@ import {
useDisclosure,
} from '@chakra-ui/react';
import { useLocalStorage } from '../../../common/hooks/useLocalStorage';
import ParamInput from './ParamInput';
import { ParamField } from './types';
import style from './ViewParamsEditor.module.scss';
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
type SavedViewParams = Record<string, ViewParamsObj>;
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj) =>
Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => {
if (typeof value === 'string' && value.length) {
newSearchParams.set(id, value);
return newSearchParams;
}
return newSearchParams;
}, new URLSearchParams());
interface EditFormDrawerProps {
paramFields: ParamField[];
}
@@ -40,8 +24,6 @@ interface EditFormDrawerProps {
export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
const [searchParams, setSearchParams] = useSearchParams();
const { isOpen, onClose, onOpen } = useDisclosure();
const { pathname } = useLocation();
const [storedViewParams, setStoredViewParams] = useLocalStorage<SavedViewParams>('ontime-views', {});
useEffect(() => {
const isEditing = searchParams.get('edit');
@@ -51,19 +33,6 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
}
}, [searchParams, onOpen]);
useEffect(() => {
const viewParamsObjFromLocalStorage = storedViewParams[pathname];
if (viewParamsObjFromLocalStorage !== undefined) {
const defaultSearchParams = getURLSearchParamsFromObj(viewParamsObjFromLocalStorage);
setSearchParams(defaultSearchParams);
}
// linter is asking for `setSearchParams` in the useEffect deps
// rule is disabled since adding `setSearchParams` results in unnecessary re-renders
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [storedViewParams, pathname]);
const onEditDrawerClose = () => {
onClose();
@@ -72,7 +41,6 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
};
const clearParams = () => {
setStoredViewParams({ ...storedViewParams, [pathname]: {} });
setSearchParams();
onClose();
};
@@ -81,9 +49,15 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
formEvent.preventDefault();
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
const newSearchParams = getURLSearchParamsFromObj(newParamsObject);
const newSearchParams = Object.entries(newParamsObject).reduce((newSearchParams, [id, value]) => {
if (typeof value === 'string' && value.length) {
newSearchParams.set(id, value);
setStoredViewParams({ ...storedViewParams, [pathname]: newParamsObject });
return newSearchParams;
}
return newSearchParams;
}, new URLSearchParams());
setSearchParams(newSearchParams);
};
@@ -132,7 +132,7 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
{
id: 'hideovertime',
title: 'Hide Overtime',
description: 'Whether to suppress overtime styles (red borders and red text)',
description: 'Whether to supress overtime styles (red borders and red text)',
type: 'boolean',
},
{
@@ -194,49 +194,6 @@ export const LOWER_THIRDS_OPTIONS: ParamField[] = [
},
];
export const BACKSTAGE_OPTIONS: ParamField[] = [
TIME_FORMAT_OPTION,
{
id: 'hidePast',
title: 'Hide past events',
description: 'Scheduler will only show upcoming events',
type: 'boolean',
},
{
id: 'stopCycle',
title: 'Stop cycling through event pages',
description: 'Schedule will not auto-cycle through events',
type: 'boolean',
},
{
id: 'eventsPerPage',
title: 'Events per page',
description: 'Sets the number of events on the page, can cause overlow',
type: 'number',
},
];
export const PUBLIC_OPTIONS: ParamField[] = [
TIME_FORMAT_OPTION,
{
id: 'hidePast',
title: 'Hide past events',
description: 'Scheduler will only show upcoming events',
type: 'boolean',
},
{
id: 'stopCycle',
title: 'Stop cycling through event pages',
description: 'Schedule will not auto-cycle through events',
type: 'boolean',
},
{
id: 'eventsPerPage',
title: 'Events per page',
description: 'Sets the number of events on the page, can cause overlow',
type: 'number',
},
];
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
TIME_FORMAT_OPTION,
{
@@ -1,5 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { GetInfo } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { APP_INFO } from '../api/apiConstants';
@@ -7,7 +6,7 @@ import { getInfo } from '../api/ontimeApi';
import { ontimePlaceholderInfo } from '../models/Info';
export default function useInfo() {
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
const { data, status, isError, refetch } = useQuery({
queryKey: APP_INFO,
queryFn: getInfo,
placeholderData: ontimePlaceholderInfo,
@@ -17,5 +16,5 @@ export default function useInfo() {
networkMode: 'always',
});
return { data, status, isError, refetch, isFetching };
return { data, status, isError, refetch };
}
@@ -6,6 +6,7 @@ export default function useClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T>,
callback: ClickOutsideEventHandler,
) {
useEffect(() => {
function handleClick(event: MouseEvent) {
const element = ref?.current;
@@ -1,7 +1,7 @@
export default function useElectronEvent() {
const isElectron = window?.process?.type === 'renderer';
const sendToElectron = (channel: string, args?: string | Record<string, unknown>) => {
const sendToElectron = (channel: string, args?: string | Record<string, any>) => {
if (isElectron) {
window?.ipcRenderer.send(channel, args);
}
@@ -100,7 +100,7 @@ export const useEventAction = () => {
// @ts-expect-error -- we know that the object is well formed now
await _addEventMutation.mutateAsync(newEvent);
} catch (error) {
logAxiosError('Failed adding event', error);
logAxiosError('Error fetching data', error);
}
},
[_addEventMutation, defaultPublic, queryClient, startTimeIsLastEnd],
+29 -11
View File
@@ -1,4 +1,10 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
export type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
@@ -20,13 +26,13 @@ const LOG_LEVEL: Record<TLogLevel, number> = {
};
const useFitText = ({
logLevel: logLevelOption = 'info',
maxFontSize = 100,
minFontSize = 20,
onFinish,
onStart,
resolution = 5,
}: TOptions = {}) => {
logLevel: logLevelOption = 'info',
maxFontSize = 100,
minFontSize = 20,
onFinish,
onStart,
resolution = 5,
}: TOptions = {}) => {
const logLevel = LOG_LEVEL[logLevelOption];
const initState = useCallback(() => {
@@ -106,7 +112,8 @@ const useFitText = ({
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
const isOverflow =
!!ref.current &&
(ref.current.scrollHeight > ref.current.offsetHeight || ref.current.scrollWidth > ref.current.offsetWidth);
(ref.current.scrollHeight > ref.current.offsetHeight ||
ref.current.scrollWidth > ref.current.offsetWidth);
const isFailed = isOverflow && fontSize === fontSizePrev;
const isAsc = fontSize > fontSizePrev;
@@ -116,7 +123,9 @@ const useFitText = ({
if (isFailed) {
isCalculatingRef.current = false;
if (logLevel <= LOG_LEVEL.info) {
console.info(`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`);
console.info(
`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`,
);
}
} else if (isOverflow) {
setState({
@@ -151,7 +160,16 @@ const useFitText = ({
fontSizeMin: newMin,
fontSizePrev: fontSize,
});
}, [calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev, onFinish, ref, resolution]);
}, [
calcKey,
fontSize,
fontSizeMax,
fontSizeMin,
fontSizePrev,
onFinish,
ref,
resolution,
]);
return { fontSize: `${fontSize}%`, ref };
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef } from "react";
/**
* @description utility hook to around setInterval
@@ -46,7 +46,7 @@ export const useLocalStorage = <T>(key: string, initialValue: T): [T, (value: T
setStoredValue(valueToStore);
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
console.log(error);
}
};
return [storedValue, setValue];
+16 -22
View File
@@ -1,25 +1,19 @@
import { GetInfo, OSCSettings } from 'ontime-types';
import { Settings } from 'ontime-types';
export const oscPlaceholderSettings: OSCSettings = {
portIn: 0,
portOut: 0,
targetIP: '',
enabledIn: false,
enabledOut: false,
subscriptions: {
onLoad: [],
onStart: [],
onPause: [],
onStop: [],
onUpdate: [],
onFinish: [],
type NetworkInterfaceType = {
name: string;
address: string;
};
export type InfoType = {
networkInterfaces: NetworkInterfaceType[];
settings: Pick<Settings, 'version' | 'serverPort'>;
};
export const ontimePlaceholderInfo: InfoType = {
networkInterfaces: [],
settings: {
version: 2,
serverPort: 4001,
},
};
export const ontimePlaceholderInfo: GetInfo = {
networkInterfaces: [],
version: '2.0.0',
serverPort: 4001,
osc: oscPlaceholderSettings,
cssOverride: '',
};
@@ -2,7 +2,7 @@ import { Settings } from 'ontime-types';
export const ontimePlaceholderSettings: Settings = {
app: 'ontime',
version: '2.0.0',
version: 2,
serverPort: 4001,
editorKey: null,
operatorKey: null,
@@ -1,6 +1,5 @@
import { resolvePath } from 'react-router-dom';
import { generateURLFromAlias, getAliasRoute, validateAlias } from '../aliases';
import { validateAlias, generateURLFromAlias, getAliasRoute } from '../aliases';
describe('An alias fails if incorrect', () => {
const testsToFail = [
@@ -12,7 +12,7 @@ test('Clamps a set of numbers correctly', () => {
{ num: -50, min: 0, max: 0, result: 0 },
{ num: 50.5, min: 0, max: 100, result: 50.5 },
{ num: 50, min: 0, max: 20.32, result: 20.32 },
{ num: 10, min: 20.32, max: 40, result: 20.32 },
{ num: 10, min: 20.32, max: 40, result: 20.32 }
];
testCases.forEach((t) => expect(clamp(t.num, t.min, t.max)).toBe(t.result));
+1 -1
View File
@@ -47,7 +47,7 @@ export const getAliasRoute = (location: Location, data: Alias[], searchParams: U
const aliasOnPage = searchParams.get('alias');
for (const d of data) {
if (aliasOnPage) {
// if the alias fits the alias on this page, but the URL is different, we redirect user to the new URL
// if the alias fits the alias on this page, but the URL is diferent, we redirect user to the new URL
// if we have the same alias and its enabled and its not empty
if (d.alias !== '' && d.enabled && d.alias === aliasOnPage) {
const newAliasPath = resolvePath(d.pathAndParams);
@@ -1,56 +0,0 @@
import axios from 'axios';
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
type FileOptions = {
name: string;
type: string;
};
type BlobOptions = {
type: string;
};
export default async function fileDownload(url: string, fileOptions: FileOptions, blobOptions: BlobOptions) {
const response = await axios({
url: `${url}/db`,
method: 'GET',
});
const headerLine = response.headers['Content-Disposition'];
let { name: fileName } = fileOptions;
const { type: fileType } = fileOptions;
const { project, rundown, userFields } = response.data;
// try and get the filename from the response
if (headerLine != null) {
const startFileNameIndex = headerLine.indexOf('"') + 1;
const endFileNameIndex = headerLine.lastIndexOf('"');
fileName = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
let fileContent = '';
if (fileType === 'json') {
fileContent = JSON.stringify(response.data);
fileName += '.json';
}
if (fileType === 'csv') {
const sheetData = makeTable(project, rundown, userFields);
fileContent = makeCSV(sheetData);
fileName += '.csv';
}
const blob = new Blob([fileContent], { type: blobOptions.type });
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', downloadUrl);
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
// Clean up the URL.createObjectURL to release resources
URL.revokeObjectURL(downloadUrl);
return;
}
+1 -1
View File
@@ -129,7 +129,7 @@ export const socketSend = (message: any) => {
}
};
export const socketSendJson = (type: string, payload?: unknown) => {
export const socketSendJson = (type: string, payload?: any) => {
socketSend(
JSON.stringify({
type,
+3 -3
View File
@@ -10,13 +10,13 @@ type ColourCombination = {
* @param bgColour
* @return {{backgroundColor, color: string}}
*/
export const getAccessibleColour = (bgColour?: string): ColourCombination => {
export const getAccessibleColour = (bgColour: string): ColourCombination => {
if (bgColour) {
try {
const textColor = Color(bgColour).isLight() ? 'black' : '#fffffa';
return { backgroundColor: bgColour, color: textColor };
} catch (_error) {
/* we do not handle errors here */
} catch (error) {
console.log(`Unable to parse colour: ${bgColour}`);
}
}
return { backgroundColor: '#000', color: '#fffffa' };
+3 -2
View File
@@ -13,8 +13,9 @@ declare global {
};
process: {
type: string;
};
}
}
}
export default {};
// eslint-disable-next-line import/no-anonymous-default-export
export default {}
+1 -1
View File
@@ -6,6 +6,6 @@ import 'vitest';
// https://github.com/testing-library/jest-dom/issues/123
declare global {
namespace Vi {
type Assertion<T = any> = TestingLibraryMatchers<T, void>;
interface Assertion<T = any> extends TestingLibraryMatchers<T, void> {}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ const withAlias = <P extends object>(Component: ComponentType<P>) => {
}
}, [data, searchParams, navigate, location]);
return <Component {...(props as P)} />;
return <Component {...props} />;
};
};
@@ -42,7 +42,7 @@ export default function MessageControl() {
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
/>
<div className={style.buttonSection}>
<label className={style.label}>Timer message blink</label>
<label className={style.label}>Timer messsage blink</label>
<label className={style.label}>Blackout timer screens</label>
<Button
className={`${data.timerMessage.timerBlink ? style.blink : ''}`}
@@ -44,11 +44,6 @@ $table-header-font-size: calc(1rem - 3px);
min-width: 2rem;
text-align: right;
font-weight: 400;
position: sticky;
left: 0;
z-index: 1;
background-color: $gray-1300;
}
}
@@ -1,6 +1,5 @@
import { useRef } from 'react';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import Color from 'color';
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import useFollowComponent from '../../common/hooks/useFollowComponent';
@@ -118,14 +117,6 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
let rowBgColour: string | undefined;
if (isSelected) {
rowBgColour = 'var(--cuesheet-running-bg-override, #D20300)'; // $red-700
} else if (row.original.colour) {
try {
// the colour is user defined and might be invalid
const colour = new Color(row.original.colour).alpha(0.25);
rowBgColour = colour.hsl().string();
} catch (_error) {
/* we do not handle errors here */
}
}
return (
@@ -21,4 +21,4 @@
border: 1px solid $white-10;
border-radius: 4px;
}
}
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { OntimeRundownEntry, ProjectData } from 'ontime-types';
import Empty from '../../common/components/state/Empty';
@@ -6,7 +6,6 @@ import { useEventAction } from '../../common/hooks/useEventAction';
import { useCuesheet } from '../../common/hooks/useSocket';
import useRundown from '../../common/hooks-query/useRundown';
import useUserFields from '../../common/hooks-query/useUserFields';
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
import Cuesheet from './Cuesheet';
@@ -21,8 +20,6 @@ export default function CuesheetWrapper() {
const { updateEvent } = useEventAction();
const featureData = useCuesheet();
const columns = useMemo(() => makeCuesheetColumns(userFields), [userFields]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [headerData, setheaderData] = useState<ProjectData | null>(null);
// Set window title
useEffect(() => {
@@ -72,76 +69,37 @@ export default function CuesheetWrapper() {
);
const exportHandler = useCallback(
(headerData: ProjectData, exportType: ExportType) => {
(headerData: ProjectData) => {
if (!headerData || !rundown || !userFields) {
return;
}
let fileName = '';
let url = '';
const sheetData = makeTable(headerData, rundown, userFields);
const csvContent = makeCSV(sheetData);
if (exportType === 'json') {
const jsonContent = JSON.stringify({
headerData,
rundown,
userFields,
});
fileName = 'ontime export.json';
const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
url = URL.createObjectURL(blob);
} else if (exportType === 'csv') {
const sheetData = makeTable(headerData, rundown, userFields);
const csvContent = makeCSV(sheetData);
fileName = 'ontime export.csv';
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
url = URL.createObjectURL(blob);
} else {
console.error('Invalid export type: ', exportType);
return;
}
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.setAttribute('download', 'ontime export.csv');
document.body.appendChild(link);
link.click();
// Clean up the URL.createObjectURL to release resources
URL.revokeObjectURL(url);
return;
},
[rundown, userFields],
);
const onModalClose = (exportType?: ExportType) => {
setIsModalOpen(false);
if (!exportType) {
return;
}
if (headerData) {
exportHandler(headerData, exportType);
}
};
const handleOpenModal = (projectData: ProjectData) => {
setheaderData(projectData);
setIsModalOpen(true);
};
if (!rundown || !userFields) {
return <Empty text='Loading...' />;
}
return (
<div className={styles.tableWrapper} data-testid='cuesheet'>
<CuesheetTableHeader handleExport={handleOpenModal} featureData={featureData} />
<CuesheetTableHeader handleCSVExport={exportHandler} featureData={featureData} />
<Cuesheet data={rundown} columns={columns} handleUpdate={handleUpdate} selectedId={featureData.selectedEventId} />
<ExportModal isOpen={isModalOpen} onClose={onModalClose} />
</div>
);
}
@@ -0,0 +1,6 @@
interface CuesheetRowProps {
row: OntimeRundownEntry;
isSelected: boolean;
}
function CuesheetRow() {}
@@ -19,8 +19,9 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
const ownRef = useRef<HTMLTableRowElement>(null);
const [isVisible, setIsVisible] = useState(false);
const bgColour = colour;
const textColour = getAccessibleColour(bgColour);
const bgFallback = 'transparent';
const bgColour = colour || bgFallback;
const textColour = bgColour === bgFallback ? undefined : getAccessibleColour(bgColour);
useLayoutEffect(() => {
const observer = new IntersectionObserver(
@@ -16,7 +16,7 @@ import CuesheetTableHeaderTimers from './CuesheetTableHeaderTimers';
import style from './CuesheetTableHeader.module.scss';
interface CuesheetTableHeaderProps {
handleExport: (headerData: ProjectData) => void;
handleCSVExport: (headerData: ProjectData) => void;
featureData: {
playback: Playback;
selectedEventIndex: number | null;
@@ -25,7 +25,7 @@ interface CuesheetTableHeaderProps {
};
}
export default function CuesheetTableHeader({ handleExport, featureData }: CuesheetTableHeaderProps) {
export default function CuesheetTableHeader({ handleCSVExport, featureData }: CuesheetTableHeaderProps) {
const followSelected = useCuesheetSettings((state) => state.followSelected);
const showSettings = useCuesheetSettings((state) => state.showSettings);
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
@@ -33,9 +33,9 @@ export default function CuesheetTableHeader({ handleExport, featureData }: Cuesh
const { isFullScreen, toggleFullScreen } = useFullscreen();
const { data: project } = useProjectData();
const exportProject = () => {
const exportCsv = () => {
if (project) {
handleExport(project);
handleCSVExport(project);
}
};
@@ -72,9 +72,9 @@ export default function CuesheetTableHeader({ handleExport, featureData }: Cuesh
{isFullScreen ? <IoContract /> : <IoExpand />}
</span>
</Tooltip>
<Tooltip openDelay={tooltipDelayFast} label='Export rundown'>
<span className={style.actionIcon} onClick={exportProject}>
Export
<Tooltip openDelay={tooltipDelayFast} label='Export rundown to CSV'>
<span className={style.actionIcon} onClick={exportCsv}>
CSV
</span>
</Tooltip>
</div>
+3 -22
View File
@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { memo, useCallback, useEffect } from 'react';
import { VStack } from '@chakra-ui/react';
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
@@ -10,12 +10,11 @@ import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
import { IoSaveOutline } from '@react-icons/all-files/io5/IoSaveOutline';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadCSV, downloadRundown } from '../../common/api/ontimeApi';
import { downloadRundown } from '../../common/api/ontimeApi';
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import useElectronEvent from '../../common/hooks/useElectronEvent';
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
import style from './MenuBar.module.scss';
@@ -101,22 +100,6 @@ const MenuBar = (props: MenuBarProps) => {
};
}, [handleKeyPress, isElectron]);
const [isModalOpen, setIsModalOpen] = useState(false);
const onModalClose = (exportType?: ExportType) => {
setIsModalOpen(false);
if (!exportType) {
return;
}
if (exportType === 'json') {
downloadRundown();
} else if (exportType === 'csv') {
downloadCSV();
}
};
return (
<VStack>
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} size='md' />
@@ -145,14 +128,12 @@ const MenuBar = (props: MenuBarProps) => {
{...buttonStyle}
icon={<IoSaveOutline />}
isDisabled={appMode === AppMode.Run}
clickHandler={() => setIsModalOpen(true)}
clickHandler={downloadRundown}
tooltip='Export project file'
aria-label='Export project file'
size='sm'
/>
<ExportModal onClose={onModalClose} isOpen={isModalOpen} />
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
@@ -1,9 +0,0 @@
@use '../../../theme/v2Styles' as *;
.buttonRow {
justify-content: space-between;
margin-top: $section-spacing;
display: flex;
gap: $section-spacing;
margin: 0 0.25rem;
}
@@ -1,32 +0,0 @@
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import styles from './ExportModal.module.scss';
export type ExportType = 'csv' | 'json';
interface ExportModalProps {
isOpen: boolean;
onClose: (type?: ExportType) => void;
}
export default function ExportModal(props: ExportModalProps) {
const { isOpen, onClose } = props;
return (
<Modal isOpen={isOpen} onClose={onClose} motionPreset='slideInBottom' size='xl' variant='ontime-small'>
<ModalOverlay />
<ModalContent>
<ModalHeader className={styles.modalHeader}>Download options</ModalHeader>
<ModalCloseButton />
<ModalBody className={styles.buttonRow}>
<Button onClick={() => onClose('csv')} variant='ontime-subtle-on-light' width='100%'>
Rundown as CSV
</Button>
<Button onClick={() => onClose('json')} variant='ontime-filled' width='100%'>
Project file
</Button>
</ModalBody>
</ModalContent>
</Modal>
);
}
@@ -21,9 +21,7 @@ import style from './SettingsModal.module.scss';
const aliasesDocsUrl = 'https://ontime.gitbook.io/v2/features/url-aliases';
// we wrap the array in an object to be simplify react-hook-form
type Aliases = {
aliases: Alias[];
};
type Aliases = { aliases: Alias[] };
export default function AliasesForm() {
const { data, status, isFetching, refetch } = useAliases();
@@ -95,7 +93,8 @@ export default function AliasesForm() {
<AlertDescription>
Custom aliases allow providing a short name for any ontime URL. <br />
It serves two primary purposes: <br />
- Providing dynamic URLs for automation or unattended screens <br />- Simplifying complex URLs
- Providing dynamic URLs for automation or unattended screens <br />
- Simplifying complex URLs
<ModalLink href={aliasesDocsUrl}>For more information, see the docs</ModalLink>
</AlertDescription>
</div>
@@ -3,12 +3,8 @@ import { UseFormRegister } from 'react-hook-form';
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
import { IoEyeOutline } from '@react-icons/all-files/io5/IoEyeOutline';
interface FormInput {
[key: string]: string;
}
interface ModalPinInputProps {
register: UseFormRegister<FormInput>;
register: UseFormRegister<any>;
formName: string;
isDisabled?: boolean;
}
@@ -3,24 +3,19 @@
.aliases {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 8px;
flex-direction: column;
width: 100%;
padding: 0.5rem 0;
padding: 8px 0;
.aliasRow {
width: 100%;
display: flex;
align-items: center;
gap: 0.5rem;
gap: 8px;
}
.grow {
flex: 1;
}
}
.url {
font-size: calc(1rem - 2px);
user-select: text;
}
@@ -1,18 +1,16 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Alert, AlertDescription, AlertIcon, AlertTitle, Input, Switch } from '@chakra-ui/react';
import { Input, Switch } from '@chakra-ui/react';
import { ViewSettings } from 'ontime-types';
import { logAxiosError } from '../../../common/api/apiUtils';
import { postViewSettings } from '../../../common/api/ontimeApi';
import { PopoverPickerRHF } from '../../../common/components/input/popover-picker/PopoverPicker';
import useInfo from '../../../common/hooks-query/useInfo';
import useViewSettings from '../../../common/hooks-query/useViewSettings';
import { mtm } from '../../../common/utils/timeConstants';
import ModalLoader from '../modal-loader/ModalLoader';
import { inputProps } from '../modalHelper';
import ModalInput from '../ModalInput';
import ModalLink from '../ModalLink';
import ModalSplitInput from '../ModalSplitInput';
import OntimeModalFooter from '../OntimeModalFooter';
@@ -20,12 +18,8 @@ import InputMillisWithString from './InputMillisWithString';
import style from './SettingsModal.module.scss';
const cssOverrideDocsUrl = 'https://ontime.gitbook.io/v2/features/custom-styling';
export default function ViewSettingsForm() {
const { data, status, refetch, isFetching } = useViewSettings();
const { data: info, isFetching: isFetchingInfo } = useInfo();
const {
control,
handleSubmit,
@@ -81,24 +75,13 @@ export default function ViewSettingsForm() {
const disableInputs = status === 'loading';
if (isFetching || isFetchingInfo) {
if (isFetching) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} id='view-settings' className={style.sectionContainer}>
<span className={style.title}>General view settings</span>
<Alert status='info' variant='ontime-on-light-info'>
<AlertIcon />
<div className={style.column}>
<AlertTitle>CSS Override</AlertTitle>
<AlertDescription>
Ontime will use the CSS file at its install location. <br />
<span className={style.url}>{info.cssOverride}</span>
<ModalLink href={cssOverrideDocsUrl}>For more information, see the docs</ModalLink>
</AlertDescription>
</div>
</Alert>
<ModalSplitInput
field='overrideStyles'
title='Override CSS Styles'
@@ -12,7 +12,8 @@ export default function UploadFile() {
const { file, setFile, progress } = useUploadModalContextStore();
const [errors, setErrors] = useState<string>('');
const [errors, setErrors] = useState<string | undefined>();
const success = false;
const clearFile = () => {
setFile(null);
@@ -34,8 +35,6 @@ export default function UploadFile() {
} catch (error) {
if (error instanceof Error) {
setErrors(error.message);
} else {
setErrors('An unexpected error occurred while validating the file.');
}
setFile(null);
}
@@ -56,11 +55,13 @@ export default function UploadFile() {
data-testid='file-input'
/>
{!file && (
<div className={style.uploadArea} onClick={handleClick} role='button'>
Click to select Ontime project or xlsx rundown
<div className={style.uploadArea} onClick={handleClick}>
Click to upload Ontime project or xlsx file
</div>
)}
{(file || errors) && <UploadEntry file={file} errors={errors} progress={progress} handleClear={clearFile} />}
{(file || errors) && (
<UploadEntry file={file} errors={errors} progress={progress} success={success} handleClear={clearFile} />
)}
</>
);
}
@@ -13,7 +13,6 @@ import { useQueryClient } from '@tanstack/react-query';
import { OntimeRundown, ProjectData, UserFields } from 'ontime-types';
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
import { PROJECT_DATA, RUNDOWN_TABLE, USERFIELDS } from '../../../common/api/apiConstants';
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils';
import {
patchData,
@@ -30,11 +29,12 @@ import OntimeFileOptions from './upload-options/OntimeFileOptions';
import UploadStepTracker from './upload-step/UploadStep';
import UploadFile from './UploadFile';
import { useUploadModalContextStore } from './uploadModalContext';
import { getPersistedOptions, isExcelFile, isOntimeFile, persistOptions } from './uploadUtils';
import { isExcelFile, isOntimeFile } from './uploadUtils';
import style from './UploadModal.module.scss';
import { PROJECT_DATA, RUNDOWN_TABLE, USERFIELDS } from '../../../common/api/apiConstants';
export type UploadStep = 'import' | 'review';
export type UploadStep = 'upload' | 'review';
interface UploadModalProps {
onClose: () => void;
@@ -46,7 +46,7 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
const { file, setProgress, clear } = useUploadModalContextStore();
const [uploadStep, setUploadStep] = useState<UploadStep>('import');
const [uploadStep, setUploadStep] = useState<UploadStep>('upload');
const [submitting, setSubmitting] = useState(false);
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
const [userFields, setUserFields] = useState<UserFields | null>(null);
@@ -57,31 +57,10 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
const ontimeFileOptions = useRef<Partial<ProjectFileImportOptions>>({});
const excelFileOptions = useRef<ExcelImportMap>(defaultExcelImportMap);
const updateOntimeFileOptions = <T extends keyof ProjectFileImportOptions>(
field: T,
value: ProjectFileImportOptions[T],
) => {
ontimeFileOptions.current = { ...ontimeFileOptions.current, [field]: value };
};
const updateExcelFileOptions = <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => {
if (excelFileOptions.current[field] !== value) {
excelFileOptions.current = { ...excelFileOptions.current, [field]: value };
}
};
// We want to populate the options with any previous options given by the user
useEffect(() => {
const excelOptions = getPersistedOptions('excel');
if (excelOptions) {
excelFileOptions.current = excelOptions;
}
}, []);
// if the modal re-opens, we want to restart all states
/* if the modal re-opens, we want to restart all states */
useEffect(() => {
clear();
setUploadStep('import');
setUploadStep('upload');
setSubmitting(false);
setRundown(null);
setUserFields(null);
@@ -103,12 +82,11 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
// TODO: we would also like to have preview for ontime project files
const options = ontimeFileOptions.current;
await handleOntimeFile(file, options);
await invalidateAllCaches();
doClose = true;
} else if (isExcelFile(file)) {
const options = excelFileOptions.current;
persistOptions({ optionType: 'excel', options });
await handleExcelFile(file, options);
await invalidateAllCaches();
}
} catch (error) {
const message = maybeAxiosError(error);
@@ -175,20 +153,20 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
};
const undoReview = () => {
setUploadStep('import');
setUploadStep('upload');
setErrors('');
};
const isImporting = uploadStep === 'import';
const isUpload = uploadStep === 'upload';
const isReview = uploadStep === 'review';
const isExcel = isExcelFile(file);
const isOntime = isOntimeFile(file);
const handleGoBack = isImporting ? undefined : undoReview;
const handleSubmit = isImporting ? handleUpload : handleFinalise;
const disableSubmit = (isImporting && !file) || (isReview && rundown === null);
const disableGoBack = isImporting;
const submitText = isImporting ? 'Import' : 'Finish';
const handleGoBack = isUpload ? undefined : undoReview;
const handleSubmit = isUpload ? handleUpload : handleFinalise;
const disableSubmit = (isUpload && !file) || (isReview && rundown === null);
const disableGoBack = isUpload;
const submitText = isUpload ? 'Upload' : 'Finish';
return (
<Modal
@@ -207,11 +185,11 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
<ModalCloseButton />
<ModalBody className={style.uploadBody}>
{isExcel && <UploadStepTracker uploadStep={uploadStep} />}
{uploadStep === 'import' ? (
{uploadStep === 'upload' ? (
<>
<UploadFile />
{isOntime && <OntimeFileOptions optionsRef={ontimeFileOptions} updateOptions={updateOntimeFileOptions} />}
{isExcel && <ExcelFileOptions optionsRef={excelFileOptions} updateOptions={updateExcelFileOptions} />}
{isOntime && <OntimeFileOptions optionsRef={ontimeFileOptions} />}
{isExcel && <ExcelFileOptions optionsRef={excelFileOptions} />}
</>
) : (
<PreviewExcel
@@ -223,6 +201,7 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
</ModalBody>
<ModalFooter>
<div className={style.feedbackSection}>{errors && <div className={style.error}>{errors}</div>}</div>
<div className={`${style.buttonSection} ${style.pad}`}>
<Button
onClick={handleGoBack}
@@ -15,7 +15,7 @@ export default function PreviewExcel(props: PreviewExcelProps) {
const { rundown, project, userFields } = props;
return (
<div className={`${style.column}`}>
<div className={`${style.column} ${style.noHover}`}>
<div className={style.title}>Review Project Data</div>
<PreviewProjectData project={project} />
<div className={style.vSpacer} />
@@ -1,4 +1,3 @@
import { Fragment } from 'react';
import { isOntimeEvent, OntimeRundown, UserFields } from 'ontime-types';
import { millisToString } from 'ontime-utils';
@@ -20,112 +19,107 @@ function booleanToText(value?: boolean) {
export default function PreviewRundown({ rundown, userFields }: PreviewRundownProps) {
return (
<div className={style.container}>
<table className={style.rundownPreview}>
<thead className={style.header}>
<tr>
<th>#</th>
<th>Type</th>
<th>Cue</th>
<th>Title</th>
<th>Subtitle</th>
<th>Presenter</th>
<th>Time Start</th>
<th>Time End</th>
<th>Duration</th>
<th>Is Public</th>
<th>Skip</th>
<th>Colour</th>
<th>Timer Type</th>
<th>End Action</th>
<th>
user0 <Tag>{userFields.user0}</Tag>
</th>
<th>
user1 <Tag>{userFields.user1}</Tag>
</th>
<th>
user2 <Tag>{userFields.user2}</Tag>
</th>
<th>
user3 <Tag>{userFields.user3}</Tag>
</th>
<th>
user4 <Tag>{userFields.user4}</Tag>
</th>
<th>
user5 <Tag>{userFields.user5}</Tag>
</th>
<th>
user6 <Tag>{userFields.user6}</Tag>
</th>
<th>
user7 <Tag>{userFields.user7}</Tag>
</th>
<th>
user8 <Tag>{userFields.user8}</Tag>
</th>
<th>
user9 <Tag>{userFields.user9}</Tag>
</th>
</tr>
</thead>
<tbody className={style.body}>
{rundown.map((event, index) => {
if (!isOntimeEvent(event)) {
return null;
}
const key = event.id;
const colour = event.colour ? getAccessibleColour(event.colour) : {};
const isPublic = booleanToText(event.isPublic);
const skip = booleanToText(event.skip);
return (
<Fragment key={key}>
<tr>
<td className={style.center}>
<Tag>{index + 1}</Tag>
</td>
<td className={style.center}>
<Tag>Event</Tag>
</td>
<td className={style.nowrap}>{event.cue}</td>
<td>{event.title}</td>
<td>{event.subtitle}</td>
<td>{event.presenter}</td>
<td>{millisToString(event.timeStart)}</td>
<td>{millisToString(event.timeEnd)}</td>
<td>{millisToString(event.duration)}</td>
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
<td>{skip && <Tag>{skip}</Tag>}</td>
<td style={{ ...colour }}>{event.colour}</td>
<td className={style.center}>
<Tag>{event.timerType}</Tag>
</td>
<td className={style.center}>
<Tag>{event.endAction}</Tag>
</td>
<td>{event.user0}</td>
<td>{event.user1}</td>
<td>{event.user2}</td>
<td>{event.user3}</td>
<td>{event.user4}</td>
<td>{event.user5}</td>
<td>{event.user6}</td>
<td>{event.user7}</td>
<td>{event.user8}</td>
<td>{event.user9}</td>
</tr>
{event.note && (
<tr>
<td colSpan={99} className={style.secondaryRow}>
Note: {event.note}
<div className={style.scrollContainer}>
<table className={style.rundownPreview}>
<thead className={style.header}>
<tr>
<th>#</th>
<th>Type</th>
<th>Cue</th>
<th>Title</th>
<th>Subtitle</th>
<th>Presenter</th>
<th>Note</th>
<th>Time Start</th>
<th>Time End</th>
<th>Duration</th>
<th>Is Public</th>
<th>Skip</th>
<th>Colour</th>
<th>Timer Type</th>
<th>End Action</th>
<th>
user0 <Tag>{userFields.user0}</Tag>
</th>
<th>
user1 <Tag>{userFields.user1}</Tag>
</th>
<th>
user2 <Tag>{userFields.user2}</Tag>
</th>
<th>
user3 <Tag>{userFields.user3}</Tag>
</th>
<th>
user4 <Tag>{userFields.user4}</Tag>
</th>
<th>
user5 <Tag>{userFields.user5}</Tag>
</th>
<th>
user6 <Tag>{userFields.user6}</Tag>
</th>
<th>
user7 <Tag>{userFields.user7}</Tag>
</th>
<th>
user8 <Tag>{userFields.user8}</Tag>
</th>
<th>
user9 <Tag>{userFields.user9}</Tag>
</th>
</tr>
</thead>
<tbody className={style.body}>
{rundown.map((event, index) => {
const key = event.id;
if (isOntimeEvent(event)) {
const colour = event.colour ? getAccessibleColour(event.colour) : {};
const isPublic = booleanToText(event.isPublic);
const skip = booleanToText(event.skip);
return (
<tr key={key}>
<td className={style.center}>
<Tag>{index + 1}</Tag>
</td>
<td className={style.center}>
<Tag>Event</Tag>
</td>
<td className={style.nowrap}>{event.cue}</td>
<td>{event.title}</td>
<td>{event.subtitle}</td>
<td>{event.presenter}</td>
<td>{event.note}</td>
<td>{millisToString(event.timeStart)}</td>
<td>{millisToString(event.timeEnd)}</td>
<td>{millisToString(event.duration)}</td>
<td>{isPublic && <Tag>{isPublic}</Tag>}</td>
<td>{skip && <Tag>{skip}</Tag>}</td>
<td style={{ ...colour }}>{event.colour}</td>
<td>
<Tag>{event.timerType}</Tag>
</td>
<td>
<Tag>{event.endAction}</Tag>
</td>
<td>{event.user0}</td>
<td>{event.user1}</td>
<td>{event.user2}</td>
<td>{event.user3}</td>
<td>{event.user4}</td>
<td>{event.user5}</td>
<td>{event.user6}</td>
<td>{event.user7}</td>
<td>{event.user8}</td>
<td>{event.user9}</td>
</tr>
)}
</Fragment>
);
})}
</tbody>
</table>
);
}
return null;
})}
</tbody>
</table>
</div>
</div>
);
}
@@ -6,9 +6,13 @@
overflow: scroll;
}
.scrollContainer {
overflow-x: scroll;
}
.rundownPreview {
font-size: calc(1rem - 2px);
overflow-x: scroll;
border-collapse: separate;
}
.header,
@@ -24,12 +28,6 @@
}
.header {
position: sticky;
top: 0;
background-color: white;
box-shadow: 0 2px $gray-50;
z-index: 3;
th {
font-weight: 200;
text-align: left;
@@ -41,6 +39,10 @@
}
.body {
tr:nth-child(odd) {
background-color: $gray-50;
}
td {
text-align: left;
vertical-align: top;
@@ -54,18 +56,5 @@
.nowrap {
white-space: nowrap;
}
.secondaryRow {
padding: 0.25em 0.25em;
background-color: $gray-50;
}
}
table tr td:first-child,
table tr th:first-child {
position: sticky;
left: 0;
z-index: 2;
background-color: white;
box-shadow: 1px 0 $gray-50;
}
@@ -1,10 +1,9 @@
@use "../../../../theme/_ontimeColours" as *;
.tag {
font-size: calc(1rem - 3px);
letter-spacing: 0.5px;
background-color: $gray-100;
color: $ui-black;
font-size: 10px;
background-color: $blue-500;
color: $pure-white;
border-radius: 2px;
padding: 0 0.25rem;
white-space: nowrap;
@@ -50,4 +50,10 @@
color: $error-red;
}
}
&.success {
.icon {
color: $green-500;
}
}
}
@@ -11,11 +11,12 @@ interface UploadEntryProps {
file: File | null;
errors?: string;
progress: number;
success: boolean;
handleClear: () => void;
}
export default function UploadEntry(props: UploadEntryProps) {
const { file, errors, progress, handleClear } = props;
const { file, errors, progress, success, handleClear } = props;
if (errors) {
return (
@@ -38,7 +39,7 @@ export default function UploadEntry(props: UploadEntryProps) {
}
return (
<div className={style.uploadedItem}>
<div className={`${style.uploadedItem} ${success ? style.success : ''}`}>
<IoClose className={style.cancelUpload} onClick={handleClear} />
<IoDocumentTextOutline className={style.icon} />
<span className={style.fileTitle}>{file.name}</span>
@@ -7,11 +7,17 @@ import style from '../UploadModal.module.scss';
interface ExcelFileOptionsProps {
optionsRef: MutableRefObject<ExcelImportMap>;
updateOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => void;
}
export default function ExcelFileOptions(props: ExcelFileOptionsProps) {
const { optionsRef, updateOptions } = props;
const { optionsRef } = props;
const updateRef = <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => {
// avoid unnecessary changes
if (optionsRef.current[field] !== value) {
optionsRef.current = { ...optionsRef.current, [field]: value };
}
};
const worksheet: TableEntry[] = [{ label: 'Worksheet', title: 'worksheet', value: optionsRef.current.worksheet }];
@@ -53,17 +59,17 @@ export default function ExcelFileOptions(props: ExcelFileOptionsProps) {
return (
<div className={style.uploadOptions}>
<div className={style.twoEqualColumn}>
<ImportMapTable title='Sheet settings' fields={worksheet} handleOnChange={updateOptions} />
<ImportMapTable title='Import options' fields={worksheet} handleOnChange={updateRef} />
</div>
<div className={style.twoEqualColumn}>
<ImportMapTable title='Timings' fields={timings} handleOnChange={updateOptions} />
<ImportMapTable title='Options' fields={options} handleOnChange={updateOptions} />
<ImportMapTable title='Timings' fields={timings} handleOnChange={updateRef} />
<ImportMapTable title='Options' fields={options} handleOnChange={updateRef} />
</div>
<div className={style.twoEqualColumn}>
<ImportMapTable title='Titles' fields={titles} handleOnChange={updateOptions} />
<ImportMapTable title='User Fields' fields={userFields} handleOnChange={updateOptions} />
<ImportMapTable title='Titles' fields={titles} handleOnChange={updateRef} />
<ImportMapTable title='User Fields' fields={userFields} handleOnChange={updateRef} />
</div>
</div>
);
@@ -26,7 +26,7 @@ export default function ImportMapTable(props: ImportMapTableProps) {
return (
<tr key={field.title}>
<td className={style.label}>
<label htmlFor={field.title}>{field.label}</label>
<label htmlFor={field.title}>{field.title}</label>
</td>
<td className={style.input}>
<Input
@@ -8,11 +8,14 @@ import style from '../UploadModal.module.scss';
interface OntimeFileOptionsProps {
optionsRef: MutableRefObject<Partial<ProjectFileImportOptions>>;
updateOptions: <T extends keyof ProjectFileImportOptions>(field: T, value: ProjectFileImportOptions[T]) => void;
}
export default function OntimeFileOptions(props: OntimeFileOptionsProps) {
const { optionsRef, updateOptions } = props;
const { optionsRef } = props;
const updateRef = <T extends keyof ProjectFileImportOptions>(field: T, value: ProjectFileImportOptions[T]) => {
optionsRef.current = { ...optionsRef.current, [field]: value };
};
return (
<div className={style.uploadOptions}>
@@ -21,7 +24,7 @@ export default function OntimeFileOptions(props: OntimeFileOptionsProps) {
<Switch
variant='ontime-on-light'
onChange={(e) => {
updateOptions('onlyRundown', e.target.checked);
updateRef('onlyRundown', e.target.checked);
}}
defaultChecked={Boolean(optionsRef.current.onlyRundown)}
/>
@@ -7,14 +7,14 @@ import type { UploadStep } from '../UploadModal';
import style from './UploadStep.module.scss';
export default function UploadStepTracker({ uploadStep }: { uploadStep: UploadStep }) {
const isImporting = uploadStep === 'import';
const isUpload = uploadStep === 'upload';
const isReview = uploadStep === 'review';
return (
<div className={style.stepRow}>
<div className={isImporting ? style.active : style.idle}>
<div className={isUpload ? style.active : style.idle}>
<IoCheckmarkCircle />
Import
Upload
</div>
<IoChevronForward className={isReview ? style.activeIcon : style.inactiveIcon} />
<div className={isReview ? style.active : style.inactive}>
@@ -1,22 +1,8 @@
import { ExcelImportMap } from 'ontime-utils';
import { ProjectFileImportOptions } from '../../../common/api/ontimeApi';
/**
* Validates a file according to the app upload contract
* @throws
* @param file
*/
export function validateFile(file: File) {
if (!file) {
throw new Error('No file to upload');
}
// Check if file is empty
if (file.size === 0) {
throw new Error('File is empty');
}
// Limit file size of a project file to around 1MB
if (file.name.endsWith('.json') && file.size > 1_000_000) {
throw new Error('File size limit (1MB) exceeded');
@@ -40,25 +26,3 @@ export function isExcelFile(file: File | null) {
export function isOntimeFile(file: File | null) {
return file?.name.endsWith('.json');
}
type PersistedOntimeOptions = {
optionType: 'ontime';
options: Partial<ProjectFileImportOptions>;
};
type PersistedExcelOptions = {
optionType: 'excel';
options: ExcelImportMap;
};
export function persistOptions(options: PersistedOntimeOptions | PersistedExcelOptions) {
localStorage.setItem(`ontime-import-options-${options.optionType}`, JSON.stringify(options.options));
}
export function getPersistedOptions(optionType: 'excel' | 'ontime') {
const options = localStorage.getItem(`ontime-import-options-${optionType}`);
if (!options) {
return null;
}
return JSON.parse(options);
}
@@ -175,9 +175,6 @@ $skip-opacity: 0.1;
font-size: calc(1rem - 3px);
color: $block-text-color;
line-height: calc(1rem - 3px);
max-height: 2rem;
overflow-y: hidden;
}
@@ -13,7 +13,7 @@ import { EventItemActions } from '../../RundownEntry';
interface BlockActionMenuProps {
enableDelete?: boolean;
showClone?: boolean;
actionHandler: (action: EventItemActions, payload?: unknown) => void;
actionHandler: (action: EventItemActions, payload?: any) => void;
className?: string;
}
@@ -1,6 +1,5 @@
/* eslint-disable react/display-name */
import { ComponentType, useMemo } from 'react';
import { SupportedEvent } from 'ontime-types';
import { useStore } from 'zustand';
import useProjectData from '../../common/hooks-query/useProjectData';
@@ -21,7 +20,7 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
const publicEvents = useMemo(() => {
if (Array.isArray(rundownData)) {
return rundownData.filter((e) => e.type === SupportedEvent.Event && e.title && e.isPublic);
return rundownData.filter((e) => e.type === 'event' && e.title && e.isPublic);
}
return [];
}, [rundownData]);
@@ -89,10 +89,7 @@
border-radius: 8px;
&.blink {
animation-name: blink;
animation-timing-function: ease-in-out;
animation-iteration-count: 3;
animation-duration: 1s;
animation: blink 0.5s ease-in-out 3;
}
}
@@ -102,11 +99,7 @@
margin-top: 2em;
padding-top: 1em;
display: flex;
}
.timer-gap {
flex: 1;
max-width: 7.5em;
gap: 7.5em;
}
.aux-timers {
@@ -166,12 +159,9 @@
@keyframes blink {
0% {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
}
50% {
background-color: var(--card-background-color-blink-override, $playback-start);
}
100% {
20% {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
}
}
}
@@ -11,7 +11,7 @@ import Schedule from '../../../common/components/schedule/Schedule';
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
import TitleCard from '../../../common/components/title-card/TitleCard';
import { BACKSTAGE_OPTIONS } from '../../../common/components/view-params-editor/constants';
import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-editor/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
@@ -92,7 +92,7 @@ export default function Backstage(props: BackstageProps) {
return (
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
<NavigationMenu />
<ViewParamsEditor paramFields={BACKSTAGE_OPTIONS} />
<ViewParamsEditor paramFields={[TIME_FORMAT_OPTION]} />
<div className='project-header'>
{general.title}
<div className='clock-container'>
@@ -130,12 +130,10 @@ export default function Backstage(props: BackstageProps) {
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
<div className='aux-timers__value'>{startedAt}</div>
</div>
<div className='timer-gap' />
<div className='aux-timers'>
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
<div className='aux-timers__value'>{expectedFinish}</div>
</div>
<div className='timer-gap' />
<div className='aux-timers'>
<div className='aux-timers__label'>{getLocalizedString('common.stage_timer')}</div>
<div className='aux-timers__value'>{stageTimer}</div>
@@ -9,7 +9,7 @@ import Schedule from '../../../common/components/schedule/Schedule';
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
import TitleCard from '../../../common/components/title-card/TitleCard';
import { PUBLIC_OPTIONS } from '../../../common/components/view-params-editor/constants';
import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-editor/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
@@ -58,7 +58,7 @@ export default function Public(props: BackstageProps) {
return (
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
<NavigationMenu />
<ViewParamsEditor paramFields={PUBLIC_OPTIONS} />
<ViewParamsEditor paramFields={[TIME_FORMAT_OPTION]} />
<div className='project-header'>
{general.title}
<div className='clock-container'>
+1 -4
View File
@@ -3,14 +3,11 @@
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
},
"extends": [
"eslint:recommended"
],
"plugins": [],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
"rules": {}
}
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "2.16.2",
"version": "2.9.0",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
@@ -14,14 +14,13 @@
"devDependencies": {
"electron": "^22.3.5",
"electron-builder": "^23.6.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"prettier": "^3.0.3"
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"prettier": "^2.8.3"
},
"scripts": {
"postinstall": "",
"lint": "eslint . --quiet",
"dev:electron": "cross-env NODE_ENV=development electron .",
"dev:electron": "NODE_ENV=development electron .",
"dist-win": "electron-builder --publish=never --x64 --win",
"dist-mac": "electron-builder --publish=never --mac",
"dist-linux": "electron-builder --publish=never --x64 --linux",
+27 -27
View File
@@ -8,23 +8,23 @@ function getApplicationMenu(isMac, askToQuit) {
return [
...(isMac
? [
{
label: 'Ontime',
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{
label: 'quit',
click: () => askToQuit(),
accelerator: 'Cmd+Q',
},
],
},
]
{
label: 'Ontime',
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{
label: 'quit',
click: () => askToQuit(),
accelerator: 'Cmd+Q',
},
],
},
]
: []),
{
label: 'File',
@@ -41,15 +41,15 @@ function getApplicationMenu(isMac, askToQuit) {
{ role: 'paste' },
...(isMac
? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
},
]
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
},
]
: [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]),
],
},
@@ -161,4 +161,4 @@ function getApplicationMenu(isMac, askToQuit) {
];
}
module.exports = { getApplicationMenu };
module.exports = { getApplicationMenu }
+2 -2
View File
@@ -13,7 +13,7 @@ function getTrayMenu(showApp, askToQuit) {
label: 'Shutdown',
click: () => askToQuit(),
},
];
]
}
module.exports = { getTrayMenu };
module.exports = { getTrayMenu }
+14 -2
View File
@@ -2,11 +2,23 @@
"env": {
"node": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint", "prettier"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"globals": {
"globalThis": false
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
}
+3 -4
View File
@@ -1,8 +1,7 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120
"printWidth": 120,
"tabWidth": 2
}
+12 -13
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "2.16.2",
"version": "2.9.0",
"exports": "./src/index.js",
"dependencies": {
"body-parser": "^1.20.0",
@@ -14,30 +14,29 @@
"express-validator": "^6.14.2",
"lowdb": "^5.0.5",
"multer": "^1.4.5-lts.1",
"node-osc": "^9.0.2",
"node-osc": "^8.0.10",
"node-xlsx": "^0.23.0",
"ontime-utils": "workspace:*",
"passport": "^0.6.0",
"passport-local": "~1.0.0",
"steno": "^3.1.0",
"ws": "^8.13.0"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^16.11.7",
"@types/node-osc": "^6.0.2",
"@types/node-osc": "^6.0.0",
"@types/websocket": "^1.0.5",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"esbuild": "^0.17.5",
"eslint": "^8.53.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint": "^8.31.0",
"eslint-plugin-prettier": "^4.2.1",
"nodemon": "^2.0.20",
"ontime-types": "workspace:*",
"prettier": "^3.0.3",
"prettier": "^2.8.3",
"shx": "^0.3.4",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"typescript": "^4.9.4",
"vitest": "^0.30.1"
},
"scripts": {
@@ -52,9 +51,9 @@
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
"lint": "eslint . --quiet",
"test": "cross-env IS_TEST=true vitest",
"test:pipeline": "cross-env IS_TEST=true vitest run",
"lint": "eslint .",
"test": "vitest",
"test:pipeline": "vitest run",
"typecheck": "tsc --noEmit",
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
}
+4 -11
View File
@@ -12,16 +12,16 @@ export class OscServer implements IAdapter {
constructor(config: OSCSettings) {
this.osc = new Server(config.portIn, '0.0.0.0');
this.osc.on('error', (error) => logger.error(LogOrigin.Rx, `OSC IN: ${error}`));
this.osc.on('error', console.error);
this.osc.on('message', (msg) => {
// message should look like /ontime/{path}/{params?} {args} where
// message should look like /ontime/{path} {args} where
// ontime: fixed message for app
// path: command to be called
// args: extra data, only used on some API entries (delay, goto)
// split message
const [, address, path, ...params] = msg[0].split('/');
const [, address, path] = msg[0].split('/');
const args = msg[1];
// get first part before (ontime)
@@ -37,14 +37,7 @@ export class OscServer implements IAdapter {
}
try {
const reply = dispatchFromAdapter(
path,
{
payload: args,
params,
},
'osc',
);
const reply = dispatchFromAdapter(path, args, 'osc');
if (reply) {
const { topic, payload } = reply;
this.osc.emit(topic, payload);
+1 -7
View File
@@ -123,13 +123,7 @@ export class SocketServer implements IAdapter {
// Protocol specific stuff handled above
try {
const reply = dispatchFromAdapter(
type,
{
payload,
},
'ws',
);
const reply = dispatchFromAdapter(type, payload, 'ws');
if (reply) {
const { topic, payload } = reply;
ws.send(topic, payload);
+1 -1
View File
@@ -4,7 +4,7 @@ import { socket } from './WebsocketAdapter.js';
* Utility function to notify clients that the REST data is stale
* @param payload -- possible patch payload
*/
export function sendRefetch(payload: unknown = null) {
export function sendRefetch(payload: any | null = null) {
socket.sendAsJson({
type: 'ontime-refetch',
payload,
+5 -26
View File
@@ -1,5 +1,3 @@
import { LogOrigin, OSCSettings } from 'ontime-types';
import 'dotenv/config';
import express from 'express';
import expressStaticGzip from 'express-static-gzip';
@@ -8,8 +6,10 @@ import cors from 'cors';
// import utils
import { join, resolve } from 'path';
import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { LogOrigin, OSCSettings } from 'ontime-types';
// Import Routes
import { router as rundownRouter } from './routes/rundownRouter.js';
@@ -31,8 +31,6 @@ import { logger } from './classes/Logger.js';
import { oscIntegration } from './services/integration-service/OscIntegration.js';
import { populateStyles } from './modules/loadStyles.js';
import { eventStore, getInitialPayload } from './stores/EventStore.js';
import { PlaybackService } from './services/PlaybackService.js';
import { RestorePoint, restoreService } from './services/RestoreService.js';
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
@@ -139,21 +137,10 @@ export const startServer = async () => {
expressServer = http.createServer(app);
socket.init(expressServer);
eventLoader.init();
// load restore point if it exists
const maybeRestorePoint = restoreService.load();
if (maybeRestorePoint) {
logger.info(LogOrigin.Server, 'Found resumable state');
PlaybackService.resume(maybeRestorePoint);
}
eventTimer.setRestoreCallback(async (newState: RestorePoint) => restoreService.save(newState));
// provide initial payload to event store
const initialPayload = getInitialPayload();
eventStore.init(initialPayload);
eventLoader.init();
eventStore.init(getInitialPayload());
expressServer.listen(serverPort, '0.0.0.0');
@@ -200,7 +187,7 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => {
}
const { success, message } = oscIntegration.init(osc);
logger.info(LogOrigin.Tx, message);
logger.info(LogOrigin.Rx, message);
if (success) {
integrationService.register(oscIntegration);
@@ -215,14 +202,6 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => {
export const shutdown = async (exitCode = 0) => {
console.log(`Ontime shutting down with code ${exitCode}`);
// clear the restore file if it was a normal exit
// 0 means it was a SIGNAL
// 1 means crash -> keep the file
// 99 means it was the UI
if (exitCode === 0 || exitCode === 99) {
await restoreService.clear();
}
expressServer?.close();
oscServer?.shutdown();
eventTimer.shutdown();
@@ -46,6 +46,7 @@ export class DataProvider {
static async clearRundown() {
data.rundown = [];
// @ts-expect-error -- not sure how to type, this is library side
await db.write();
}
@@ -99,6 +100,7 @@ export class DataProvider {
}
static async persist() {
// @ts-expect-error -- not sure how to type, this is library side
await db.write();
}
@@ -11,7 +11,7 @@ describe('safeMerge', () => {
},
settings: {
app: 'ontime',
version: '2.0.0',
version: 2,
serverPort: 4001,
editorKey: null,
operatorKey: null,
@@ -84,7 +84,7 @@ describe('safeMerge', () => {
const mergedData = safeMerge(existing, newData);
expect(mergedData.settings).toEqual({
app: 'ontime',
version: '2.0.0',
version: 2,
serverPort: 3000,
operatorKey: null,
editorKey: null,
@@ -144,7 +144,7 @@ describe('safeMerge', () => {
},
settings: {
app: 'ontime',
version: '2.0.0',
version: 2,
serverPort: 4001,
operatorKey: null,
editorKey: null,
@@ -23,18 +23,6 @@ export class EventLoader {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
instance = this;
this.eventNow = null;
this.publicEventNow = null;
this.eventNext = null;
this.publicEventNext = null;
this.loaded = {
selectedEventIndex: null,
selectedEventId: null,
selectedPublicEventId: null,
nextEventId: null,
nextPublicEventId: null,
numEvents: 0,
};
}
// we need to delay init until the store is ready
@@ -83,7 +71,7 @@ export class EventLoader {
* @param {string} eventId
* @return {object | undefined}
*/
static getEventWithId(eventId): OntimeEvent | undefined {
static getEventWithId(eventId) {
const timedEvents = EventLoader.getTimedEvents();
return timedEvents.find((event) => event.id === eventId);
}
@@ -180,10 +168,8 @@ export class EventLoader {
this.loaded.selectedEventIndex = nowIndex;
this.loaded.selectedEventId = currentEvent?.id || null;
this.loaded.numEvents = timedEvents.length;
this.loaded.nextEventId = nextEvent?.id || null;
this.loaded.nextPublicEventId = nextPublicEvent?.id || null;
this._loadEvent();
this.loaded.nextEventId = nextEvent.id;
this.loaded.nextPublicEventId = nextPublicEvent.id;
return { currentEvent, nextEvent, timeToNext };
}
@@ -237,7 +223,7 @@ export class EventLoader {
* loads an event given its id
* @param {object} event
*/
loadEvent(event?: OntimeEvent) {
loadEvent(event) {
if (typeof event === 'undefined') {
return null;
}
@@ -283,7 +269,6 @@ export class EventLoader {
// check if current is also public
if (event.isPublic) {
this.publicEventNow = event;
this.loaded.selectedPublicEventId = event.id;
} else {
// assume there is no public event
this.publicEventNow = null;
+1 -1
View File
@@ -3,10 +3,10 @@ export const config = {
testdb: 'test-db',
directory: 'db',
filename: 'db.json',
tablename: 'events',
},
styles: {
directory: 'styles',
filename: 'override.css',
},
restoreFile: 'ontime.restore',
};
@@ -1,69 +0,0 @@
import { LogOrigin, OntimeEvent } from 'ontime-types';
import { EventLoader } from '../classes/event-loader/EventLoader.js';
import { editEvent } from '../services/rundown-service/RundownService.js';
import { coerceString, coerceNumber, coerceBoolean } from '../utils/coerceType.js';
import { logger } from '../classes/Logger.js';
const whitelistedPayload = {
title: coerceString,
subtitle: coerceString,
presenter: coerceString,
note: coerceString,
cue: coerceString,
duration: coerceNumber,
isPublic: coerceBoolean,
skip: coerceBoolean,
colour: coerceString,
user0: coerceString,
user1: coerceString,
user2: coerceString,
user3: coerceString,
user4: coerceString,
user5: coerceString,
user6: coerceString,
user7: coerceString,
user8: coerceString,
user9: coerceString,
};
export function parse(field: string, value: unknown) {
if (!Object.hasOwn(whitelistedPayload, field)) {
throw new Error(`Field ${field} not permitted`);
}
const parserFn = whitelistedPayload[field];
return parserFn(value);
}
/**
* Updates a property of the event with the given id
* @param {string} eventId
* @param {keyof OntimeEvent} propertyName
* @param {OntimeEvent[typeof propertyName]} newValue
*/
export function updateEvent(
eventId: string,
propertyName: keyof OntimeEvent,
newValue: OntimeEvent[typeof propertyName],
) {
const event = EventLoader.getEventWithId(eventId);
if (event) {
const propertiesToUpdate = { [propertyName]: newValue };
// Handles the special case for duration
// needs to be converted to milliseconds
if (propertyName === 'duration') {
propertiesToUpdate.duration = (newValue as number) * 1000;
propertiesToUpdate.timeEnd = event.timeStart + propertiesToUpdate.duration;
}
editEvent({ id: eventId, ...propertiesToUpdate }).then(() => {
logger.info(LogOrigin.Playback, `Updated ${propertyName} of event with ID ${eventId} to ${newValue}`);
});
} else {
throw new Error(`Event with ID ${eventId} not found`);
}
}
@@ -1,27 +1,9 @@
import { OntimeEvent } from 'ontime-types';
import { messageService } from '../services/message-service/MessageService.js';
import { PlaybackService } from '../services/PlaybackService.js';
import { eventStore } from '../stores/EventStore.js';
import { parse, updateEvent } from './integrationController.config.js';
import { isKeyOfType } from 'ontime-types/src/utils/guards.js';
import { event } from '../models/eventsDefinition.js';
//TODO: re-throwing the error does not add any extra information or value
export function dispatchFromAdapter(
type: string,
args: {
payload: unknown;
params?: Array<string>;
},
_source?: 'osc' | 'ws',
) {
const payload = args.payload;
const typeComponents = type.toLowerCase().split('/');
const mainType = typeComponents[0];
const params = args.params || [];
switch (mainType) {
export function dispatchFromAdapter(type: string, payload: unknown, source?: 'osc' | 'ws') {
switch (type.toLowerCase()) {
case 'test-ontime': {
return { topic: 'hello' };
}
@@ -175,19 +157,6 @@ export function dispatchFromAdapter(
PlaybackService.roll();
break;
}
case 'addtime': {
const time = Number(payload);
if (isNaN(time)) {
throw new Error(`Time not recognised ${payload}`);
}
try {
PlaybackService.addTime(time);
} catch (error) {
throw new Error(`Could not add time: ${error}`);
}
break;
}
//deprecated
case 'delay': {
const delayTime = Number(payload);
if (isNaN(delayTime)) {
@@ -253,23 +222,6 @@ export function dispatchFromAdapter(
return { topic: 'timer', payload: timer };
}
// ontime/change/{eventID}/{propertyName}
case 'change': {
if (params.length < 2) {
throw new Error('Too few parameters, 3 expected');
}
if (payload === undefined) {
throw new Error('No payload found');
}
const eventID = params[0];
const propertyName = params[1] as keyof OntimeEvent;
if (!isKeyOfType(propertyName, event)) {
throw new Error(`Cannot update unknown event property ${propertyName}`);
}
const parsedPayload = parse(propertyName, payload);
return updateEvent(eventID, propertyName, parsedPayload);
}
default: {
throw new Error(`Unhandled message ${type}`);
}
+20 -25
View File
@@ -1,6 +1,6 @@
import { Alias, DatabaseModel, GetInfo, LogOrigin, ProjectData } from 'ontime-types';
import { Alias, DatabaseModel, LogOrigin, ProjectData } from 'ontime-types';
import { RequestHandler, Request, Response } from 'express';
import { RequestHandler } from 'express';
import fs from 'fs';
import { networkInterfaces } from 'os';
@@ -9,7 +9,7 @@ import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { PlaybackService } from '../services/PlaybackService.js';
import { eventStore } from '../stores/EventStore.js';
import { isDocker, pathToStartStyles, resolveDbPath } from '../setup.js';
import { isDocker, resolveDbPath } from '../setup.js';
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
import { logger } from '../classes/Logger.js';
import { deleteAllEvents, notifyChanges } from '../services/rundown-service/RundownService.js';
@@ -45,6 +45,8 @@ export const dbDownload = async (req, res) => {
});
};
// TODO: docs
// TODO: cleanup usage
/**
* Parses a file and returns the result objects
* @param file
@@ -105,16 +107,15 @@ const getNetworkInterfaces = () => {
return results;
};
// Create controller for GET request to '/ontime/info'
// Create controller for POST request to '/ontime/info'
// Returns -
export const getInfo = async (req: Request, res: Response<GetInfo>) => {
export const getInfo = async (req, res) => {
const { version, serverPort } = DataProvider.getSettings();
const osc = DataProvider.getOsc();
// get nif and inject localhost
const ni = getNetworkInterfaces();
ni.unshift({ name: 'localhost', address: '127.0.0.1' });
const cssOverride = pathToStartStyles;
// send object with network information
res.status(200).send({
@@ -122,7 +123,6 @@ export const getInfo = async (req: Request, res: Response<GetInfo>) => {
version,
serverPort,
osc,
cssOverride,
});
};
@@ -151,7 +151,7 @@ export const postAliases = async (req, res) => {
await DataProvider.setAliases(newAliases);
res.status(200).send(newAliases);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -174,7 +174,7 @@ export const postUserFields = async (req, res) => {
await DataProvider.setUserFields(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -208,16 +208,11 @@ export const postSettings = async (req, res) => {
const settings = DataProvider.getSettings();
const editorKey = extractPin(req.body?.editorKey, settings.editorKey);
const operatorKey = extractPin(req.body?.operatorKey, settings.operatorKey);
const serverPort = Number(req.body?.serverPort);
if (isNaN(serverPort)) {
return res.status(400).send(`Invalid value found for server port: ${req.body?.serverPort}`);
}
const hasChangedPort = settings.serverPort !== serverPort;
if (isDocker && hasChangedPort) {
if (isDocker && req.body?.serverPort) {
return res.status(403).json({ message: 'Can`t change port when running inside docker' });
}
const serverPort = parseInt(req.body?.serverPort ?? settings.serverPort, 10);
let timeFormat = settings.timeFormat;
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
@@ -237,7 +232,7 @@ export const postSettings = async (req, res) => {
await DataProvider.setSettings(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -272,7 +267,7 @@ export const postViewSettings = async (req, res) => {
await DataProvider.setViewSettings(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -296,11 +291,11 @@ export const postOscSubscriptions = async (req, res) => {
// TODO: this update could be more granular, checking that relevant data was changed
const { message } = oscIntegration.init(oscSettings);
logger.info(LogOrigin.Tx, message);
logger.info(LogOrigin.Rx, message);
res.send(oscSettings).status(200);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -317,11 +312,11 @@ export const postOSC = async (req, res) => {
// TODO: this update could be more granular, checking that relevant data was changed
const { message } = oscIntegration.init(oscSettings);
logger.info(LogOrigin.Tx, message);
logger.info(LogOrigin.Rx, message);
res.send(oscSettings).status(200);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -350,12 +345,12 @@ export async function patchPartialProjectFile(req, res) {
}
res.status(200).send();
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
}
/**
* uploads, parses and applies the data from a given file
* uploads and parses a given file
*/
export const dbUpload = async (req, res) => {
if (!req.file) {
@@ -411,6 +406,6 @@ export const postNew: RequestHandler = async (req, res) => {
await deleteAllEvents();
res.status(201).send(newData);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -13,14 +13,14 @@ export const pbStart = async (req, res) => {
const { eventId, eventIndex } = req.query;
if (eventId) {
const success = PlaybackService.startById(eventId);
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event ID' });
success ? res.sendStatus(202) : res.status(400).send('Invalid event ID');
} else if (eventIndex) {
const index = Number(eventIndex);
if (!isNaN(index)) {
const success = PlaybackService.startByIndex(eventIndex - 1);
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event index' });
success ? res.sendStatus(202) : res.status(400).send('Invalid event index');
} else {
res.status(400).send({ message: 'Invalid event index' });
res.status(400).send('Invalid event index');
}
} else {
PlaybackService.start();
@@ -69,17 +69,17 @@ export const pbLoad = async (req, res) => {
const { eventId, eventIndex } = req.query;
if (eventId) {
const success = PlaybackService.loadById(eventId);
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event ID' });
success ? res.sendStatus(202) : res.status(400).send('Invalid event ID');
} else if (eventIndex) {
const index = Number(eventIndex);
if (!isNaN(index)) {
const success = PlaybackService.loadByIndex(eventIndex - 1);
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event index' });
success ? res.sendStatus(202) : res.status(400).send('Invalid event index');
} else {
res.status(400).send({ message: 'Invalid event index' });
res.status(400).send('Invalid event index');
}
} else {
res.status(400).send({ message: 'No event given' });
res.status(400).send('No event given');
}
};
@@ -30,6 +30,6 @@ export const postProject: RequestHandler = async (req, res) => {
const newData = await DataProvider.setProjectData(newEvent);
res.status(200).send(newData);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -29,7 +29,7 @@ export const rundownPost: RequestHandler = async (req, res) => {
const newEvent = await addEvent(req.body);
res.status(201).send(newEvent);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -44,7 +44,7 @@ export const rundownPut: RequestHandler = async (req, res) => {
const event = await editEvent(req.body);
res.status(200).send(event);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -58,7 +58,7 @@ export const rundownReorder: RequestHandler = async (req, res) => {
const event = await reorderEvent(eventId, from, to);
res.status(200).send(event);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -72,7 +72,7 @@ export const rundownSwap: RequestHandler = async (req, res) => {
await swapEvents(from, to);
res.sendStatus(200);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -83,7 +83,7 @@ export const rundownApplyDelay: RequestHandler = async (req, res) => {
await applyDelay(req.params.eventId);
res.sendStatus(200);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -94,7 +94,7 @@ export const deleteEventById: RequestHandler = async (req, res) => {
await deleteEvent(req.params.eventId);
res.sendStatus(204);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
@@ -105,6 +105,6 @@ export const rundownDelete: RequestHandler = async (req, res) => {
await deleteAllEvents();
res.sendStatus(204);
} catch (error) {
res.status(400).send({ message: error.toString() });
res.status(400).send(error);
}
};
+1 -2
View File
@@ -1,5 +1,4 @@
import { DatabaseModel } from 'ontime-types';
import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
export const dbModel: DatabaseModel = {
rundown: [],
@@ -13,7 +12,7 @@ export const dbModel: DatabaseModel = {
},
settings: {
app: 'ontime',
version: ONTIME_VERSION,
version: 2,
serverPort: 4001,
editorKey: null,
operatorKey: null,
+1 -1
View File
@@ -67,7 +67,7 @@ async function loadDb() {
return { db, data };
}
export let db = {} as Low<DatabaseModel>;
export let db = {};
export let data = {} as DatabaseModel;
export const dbLoadingProcess = loadDb();

Some files were not shown because too many files have changed in this diff Show More