Compare commits

...

87 Commits

Author SHA1 Message Date
Carlos Valente e2c9ee31ad bump version to 3.16.1 2025-08-15 19:54:38 +02:00
Carlos Valente bfd85855d4 fix: prevent storage key conflicts between stages 2025-08-15 19:54:38 +02:00
Carlos Valente 20859abda9 chore: add demo images to github 2025-08-09 07:01:01 +02:00
Alex Christoffer Rasmussen 1025347c08 refactor: add version to session endpoint (#1659) 2025-06-23 20:42:53 +02:00
Carlos Valente 43988ea12b fix: stale ids in cuesheet (#1657) 2025-06-23 19:56:53 +02:00
Alex Christoffer Rasmussen 722e045b20 Revert "Session version" (#1649)
* Revert "refactor: add version to session endpoint"

This reverts commit cd40a6e55e.

* Revert "Remove public event feature (#1645)"

This reverts commit 08d9e24871.

* Revert "Fix: rearrange playing event (#1640)"

This reverts commit 0649678dca.

* Revert "refactor: style tweaks to edit css modal"

This reverts commit a00ec2d02a.

* Revert "refactor: remove usages of framer-motion"

This reverts commit 54a74ccc2a.

* Revert "Upgrade expressjs (#1633)"

This reverts commit 6f3ab274bd.

* Revert "Refactor: require trigger in all events objects (#1636)"

This reverts commit 90870ecfb6.

* Revert "Fix: Correct boundary condition in applyDelay"

This reverts commit b640e0e181.

* Revert "let vite be the proxy to the dev server (#1630)"

This reverts commit c41fe824cf.

* Revert "refactor: migrate custom fields to transactions"

This reverts commit 62c8319d70.

* Revert "refactor: simplify validations"

This reverts commit b1d23467a2.

* Revert "refactor: create transaction system and apply to adding entry (#1620)"

This reverts commit 9a62daf047.

* Revert "Refactor: better rounding (#1594)"

This reverts commit b9ab1c6fd7.

* Revert "refactor: improve reorder logic"

This reverts commit c1054711b0.

* Revert "chore: simplify URLs"

This reverts commit a4d4f29a37.

* Revert "refactor: order is single source of truth"

This reverts commit 2793aadea0.

* Revert "refactor: refetch targets is enum"

This reverts commit e7cfb7d9d9.

* Revert "refactor: small ux improvements"

This reverts commit 256a851c9b.

* Revert "refactor: remove trivially inferred numEvents"

This reverts commit 4ab9c81cb8.

* Revert "feat: duplicate groups"

This reverts commit 2f13d6c89e.

* Revert "feat: create group from entry selection"

This reverts commit f1f7bad25e.

* Revert "feat: create block from rundown empty"

This reverts commit a8b52a48f7.

* Revert "fix: collapsed blocks dont render children"

This reverts commit cd0999b2ab.

* Revert "refactor: type cleanup and test improvements"

This reverts commit b4c60f3f04.

* Revert "feat: allow dissolving a block"

This reverts commit 5cefad3666.

* Revert "fix: uncontrolled prop on controlled component"

This reverts commit 7a6ecd8c34.

* Revert "refactor: improve return of reorder"

This reverts commit ba96ecfd91.

* Revert "refactor: mutations on batch elements must have IDs"

This reverts commit 7bed3757f2.

* Revert "chore: upgrade dependencies"

This reverts commit e2e755b1d2.

* Revert "refactor: extract utility to merge two arrays"

This reverts commit a77d23109d.

* Revert "refactor: change network mode defaults"

This reverts commit 0eb3b8d382.

* Revert "fix: delete nested events"

This reverts commit 0021185288.

* Revert "fix: add event at end of block"

This reverts commit 94c72ff4f6.

* Revert "refactor: make finder available in exported rundown"

This reverts commit e4c08dc9b2.

* Revert "assert non null and update test (#1604)"

This reverts commit ec74af0d62.

* Revert "Fix project renumber (#1597)"

This reverts commit b6d72dd082.

* Revert "Refactor: WebSocket from flush queue to one patch (#1595)"

This reverts commit 31c311daf0.

* Revert "Refactor: ms for api calls (#1593)"

This reverts commit e9b3cc6090.

* Revert "fix test (#1601)"

This reverts commit 543b04a097.

* Revert "fix: rebase master"

This reverts commit d39b85b6e6.

* Revert "chore: correct test path"

This reverts commit bbe107bb2b.

* Revert "refactor: extract rundown parsing"

This reverts commit c616240db1.

* Revert "chore: improve convention entry <> event"

This reverts commit 166be66ce3.

* Revert "refactor: maintain flat orders"

This reverts commit 4180d0a337.

* Revert "refactor: implement operations on nested events"

This reverts commit 78108e316c.

* Revert "refactor: process events in rundown"

This reverts commit 3bb8b70915.

* Revert "chore: improve convention entry <> event"

This reverts commit 1c4f13a0ed.

* Revert "chore: rename currentBlock > parent"

This reverts commit 3ca0abad53.

* Revert "refactor: fix delay positioning in gaps"

This reverts commit 030c8f897f.

* Revert "refactor(e2e): skip flaky test"

This reverts commit 68175cfa3b.

* Revert "refactor: improve project loading"

This reverts commit fd8f757851.

* Revert "refactor: gather group metadata"

This reverts commit 876d111c61.

* Revert "refactor: swap maintains schedule"

This reverts commit 730cb95c04.

* Revert "chore: rename files"

This reverts commit 3c388d4fb5.

* Revert "refactor: restructure model to contain an object of rundowns"

This reverts commit 2e23718d73.

* Revert "refactor: clearer relationship on rundown elements"

This reverts commit 89ea8c470b.

* Revert "refactor: use strict typing"

This reverts commit 178640bfc4.

* Revert "refactor: remove stop as a possible end action"

This reverts commit 4ed38340e0.

* Revert "refactor: restructure model to contain an object of rundowns"

This reverts commit 69eb9a5eff.

* Revert "chore: remove IDE files"

This reverts commit 351425127a.

* Revert "refactor: remove unused and legacy code"

This reverts commit 95f2ba37cc.
2025-06-20 12:28:20 +02:00
Carlos Valente ad6804019b bump version to 3.16.0 2025-06-20 07:41:39 +02:00
Carlos Valente cd40a6e55e refactor: add version to session endpoint 2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen 08d9e24871 Remove public event feature (#1645) 2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen 0649678dca Fix: rearrange playing event (#1640)
* creat failing test

* find and pass index of eventNow in case it moved

* fix cache invalidate edits should also recalculate timer

* use coluor as test indicator

* fixed empty cue guard

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
2025-06-19 18:53:28 +02:00
google-labs-jules[bot] a00ec2d02a refactor: style tweaks to edit css modal 2025-06-19 18:53:28 +02:00
google-labs-jules[bot] 54a74ccc2a refactor: remove usages of framer-motion 2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen 6f3ab274bd Upgrade expressjs (#1633)
* upgrade expressjs

* migration

* reenable test

* extend timeout on download test

* fixup! migration

* move empty body test from controller to validator

* enusre not empty

* extract validation function

* fixup! reenable test

* remove thin controllers

* disable e2e test of project file download
2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen 90870ecfb6 Refactor: require trigger in all events objects (#1636)
* reqire trigger in all events

* use structuredClone when cloning an event
2025-06-19 18:53:28 +02:00
google-labs-jules[bot] b640e0e181 Fix: Correct boundary condition in applyDelay
The `applyDelay` function had a condition that incorrectly used `rundown.order.length` instead of `rundownMetadata.flatEntryOrder.length` to check if a delay entry was the last in the sequence. `rundown.order` only contains top-level entries, while `flatEntryOrder` contains all entries, including those within blocks, which is the relevant list for this check.

This commit corrects the condition to use `rundownMetadata.flatEntryOrder.length`.

Existing tests in `rundown.dao.test.ts` (specifically the test `removes delays in last position without applying changes`) already cover this scenario and pass with the correction, ensuring the fix behaves as expected.
2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen c41fe824cf let vite be the proxy to the dev server (#1630) 2025-06-19 18:53:28 +02:00
Carlos Valente 62c8319d70 refactor: migrate custom fields to transactions
refactor: extract functions to api domain

refactor: strict custom field parsing

refactor: remove rundown cache utilities

refactor: directory restructure
2025-06-19 18:53:28 +02:00
Carlos Valente b1d23467a2 refactor: simplify validations 2025-06-19 18:53:28 +02:00
Carlos Valente 9a62daf047 refactor: create transaction system and apply to adding entry (#1620)
* refactor: create transaction system and apply to adding entry

* refactor: migrate edit mutations to transaction

* refactor: migrate delete mutation to transaction

* refactor: migrate reorder mutation to transaction

* refactor: migrate apply delay to transaction

* refactor: migrate swapEvents to transaction

* refactor: migrate clone to transaction

* refactor: migrate group/ungroup transactions

* refactor: simplify mutations

* refactor: migrate getters

* chore: add tests to processRundown()
2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen b9ab1c6fd7 Refactor: better rounding (#1594) 2025-06-19 18:53:28 +02:00
Carlos Valente c1054711b0 refactor: improve reorder logic 2025-06-19 18:53:28 +02:00
Carlos Valente a4d4f29a37 chore: simplify URLs 2025-06-19 18:53:28 +02:00
Carlos Valente 2793aadea0 refactor: order is single source of truth 2025-06-19 18:53:28 +02:00
Carlos Valente e7cfb7d9d9 refactor: refetch targets is enum 2025-06-19 18:53:28 +02:00
Carlos Valente 256a851c9b refactor: small ux improvements
- rename dissolve > ungroup
- prevent ondrag when clicking
- add untitled as block title fallback
- move block action to context menu
2025-06-19 18:53:28 +02:00
Carlos Valente 4ab9c81cb8 refactor: remove trivially inferred numEvents 2025-06-19 18:53:28 +02:00
Carlos Valente 2f13d6c89e feat: duplicate groups 2025-06-19 18:53:28 +02:00
Carlos Valente f1f7bad25e feat: create group from entry selection 2025-06-19 18:53:28 +02:00
Carlos Valente a8b52a48f7 feat: create block from rundown empty 2025-06-19 18:53:28 +02:00
Carlos Valente cd0999b2ab fix: collapsed blocks dont render children 2025-06-19 18:53:28 +02:00
Carlos Valente b4c60f3f04 refactor: type cleanup and test improvements 2025-06-19 18:53:28 +02:00
Carlos Valente 5cefad3666 feat: allow dissolving a block 2025-06-19 18:53:28 +02:00
Carlos Valente 7a6ecd8c34 fix: uncontrolled prop on controlled component 2025-06-19 18:53:28 +02:00
Carlos Valente ba96ecfd91 refactor: improve return of reorder 2025-06-19 18:53:28 +02:00
Carlos Valente 7bed3757f2 refactor: mutations on batch elements must have IDs 2025-06-19 18:53:28 +02:00
Carlos Valente e2e755b1d2 chore: upgrade dependencies 2025-06-19 18:53:28 +02:00
Carlos Valente a77d23109d refactor: extract utility to merge two arrays 2025-06-19 18:53:28 +02:00
Carlos Valente 0eb3b8d382 refactor: change network mode defaults 2025-06-19 18:53:28 +02:00
Carlos Valente 0021185288 fix: delete nested events 2025-06-19 18:53:28 +02:00
Carlos Valente 94c72ff4f6 fix: add event at end of block 2025-06-19 18:53:28 +02:00
Carlos Valente e4c08dc9b2 refactor: make finder available in exported rundown 2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen ec74af0d62 assert non null and update test (#1604) 2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen b6d72dd082 Fix project renumber (#1597)
* fix: generateUniqueFileName

* loadProject should not generate new names

* update comments

* extract and test getProjectNumber

* spell

* finish jsdoc

* use getProjectNumber

* cleanup loadProject

* create a `incrementProjectNumber` function

* spelling

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen 31c311daf0 Refactor: WebSocket from flush queue to one patch (#1595)
* change flush to one patch

* remove unused types

* create batch

* merge patch into eventStore

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen e9b3cc6090 Refactor: ms for api calls (#1593)
* use ms for api calls

* change it in UI

* update comment

* rename variables

* refactor: addtime api logic
2025-06-19 18:53:28 +02:00
Alex Christoffer Rasmussen 543b04a097 fix test (#1601)
* fix test

* add comment
2025-06-19 18:53:28 +02:00
Carlos Valente d39b85b6e6 fix: rebase master 2025-06-19 18:53:28 +02:00
Carlos Valente bbe107bb2b chore: correct test path 2025-06-19 18:53:28 +02:00
Carlos Valente c616240db1 refactor: extract rundown parsing
refactor: implement groups in editor
2025-06-19 18:53:28 +02:00
Carlos Valente 166be66ce3 chore: improve convention entry <> event 2025-06-19 18:53:28 +02:00
Carlos Valente 4180d0a337 refactor: maintain flat orders 2025-06-19 18:53:28 +02:00
Carlos Valente 78108e316c refactor: implement operations on nested events 2025-06-19 18:53:28 +02:00
Carlos Valente 3bb8b70915 refactor: process events in rundown 2025-06-19 18:53:28 +02:00
Carlos Valente 1c4f13a0ed chore: improve convention entry <> event 2025-06-19 18:53:28 +02:00
Carlos Valente 3ca0abad53 chore: rename currentBlock > parent 2025-06-19 18:53:28 +02:00
Carlos Valente 030c8f897f refactor: fix delay positioning in gaps 2025-06-19 18:53:28 +02:00
Carlos Valente 68175cfa3b refactor(e2e): skip flaky test 2025-06-19 18:53:28 +02:00
Carlos Valente fd8f757851 refactor: improve project loading 2025-06-19 18:53:28 +02:00
Carlos Valente 876d111c61 refactor: gather group metadata 2025-06-19 18:53:28 +02:00
Carlos Valente 730cb95c04 refactor: swap maintains schedule 2025-06-19 18:53:28 +02:00
Carlos Valente 3c388d4fb5 chore: rename files 2025-06-19 18:53:28 +02:00
Carlos Valente 2e23718d73 refactor: restructure model to contain an object of rundowns 2025-06-19 18:53:28 +02:00
Carlos Valente 89ea8c470b refactor: clearer relationship on rundown elements 2025-06-19 18:53:28 +02:00
Carlos Valente 178640bfc4 refactor: use strict typing 2025-06-19 18:53:28 +02:00
Carlos Valente 4ed38340e0 refactor: remove stop as a possible end action 2025-06-19 18:53:28 +02:00
Carlos Valente 69eb9a5eff refactor: restructure model to contain an object of rundowns 2025-06-19 18:53:28 +02:00
Carlos Valente 351425127a chore: remove IDE files 2025-06-19 18:53:28 +02:00
Carlos Valente 95f2ba37cc refactor: remove unused and legacy code
- remove legacy migrations
- remove unused server code
- remove unused UI code
2025-06-19 18:53:28 +02:00
jwetzell fe8bbd5003 add electron and esbuild to allow list for postinstall builds 2025-06-06 20:25:46 +02:00
jwetzell c6e38a2b4a upgrade vite to v6 (#1626) 2025-06-05 06:27:59 +02:00
jwetzell 56f18fe71c update node and pnpm versions in README 2025-06-05 06:13:04 +02:00
jwetzell 0b9306de82 update pnpm to latest v10 2025-06-05 06:13:04 +02:00
jwetzell 3b1e6cf33d update node version in workflows 2025-06-05 06:13:04 +02:00
jwetzell dda219ebbe update NodeJS to v22 2025-06-05 06:13:04 +02:00
jwetzell 8bb771e5c6 upgrade electron and electron-builder 2025-06-05 06:13:04 +02:00
Shobhit Nagpal 8723bcbd33 refactor: block event xlsx import (#1623) 2025-06-02 06:56:46 +02:00
Shobhit Nagpal 0dcea4f2d7 fix: pass empty string fallback when parsing fields for export (#1616) 2025-05-25 19:58:41 +02:00
Shobhit Nagpal 696c016c90 feat: custom data for projects (#1571) 2025-05-23 09:04:57 +02:00
Shobhit Nagpal eed6373dbf refactor: change offset classes depending on playback state (#1611)
* refactor: assign offsetClasses based on playback state
2025-05-23 09:04:57 +02:00
Shobhit-Nagpal ac61f257c0 fix: import preview enable after error 2025-05-13 21:24:14 +02:00
Carlos Valente 8d8625aa77 bump version to 3.15.2 2025-05-13 21:24:14 +02:00
Alex Christoffer Rasmussen 1e1aa4bca5 Fix: sheet export (#1606)
* add util to get keys from labels

* look for existing keys first then create new

* save ontime key to rundown metadata

* update test
2025-05-12 13:56:25 +02:00
Alex Christoffer Rasmussen 1f0cde6d0d fix: relative offset with count to end (#1602)
* refactor merge absolute and relative offset calculation

* update test
2025-05-07 22:34:25 +02:00
Carlos Valente c4b66a9ed3 bump version to 3.15.1 2025-04-30 20:28:42 +02:00
Carlos Valente 133b3aa819 refactor: create hash of table state 2025-04-30 20:23:49 +02:00
Carlos Valente 9566fadbbb chore: upgrade table dependencies 2025-04-30 20:23:49 +02:00
Carlos Valente 62570899e8 fix: issue when user hides all columns 2025-04-30 20:23:49 +02:00
81 changed files with 2335 additions and 841 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

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