mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-04 23:18:01 +00:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 147862e647 | |||
| 12bc323e57 | |||
| de8f9ef587 | |||
| 217402050d | |||
| de75d55849 | |||
| c4e6215b29 | |||
| 57315595a5 | |||
| fc7ed6bd20 | |||
| 750927dc8d | |||
| 00d2ae0a7c | |||
| 56c4d68bae | |||
| a02cdf71c0 | |||
| 25030debe4 | |||
| 27243c4432 | |||
| 22559c59d4 | |||
| 700948bfbb | |||
| 960a39fbf4 | |||
| d986f438d9 | |||
| c2012bd9e3 | |||
| 1f0f92371f | |||
| 4b81725e80 | |||
| 846eaf569d | |||
| ccf1ddf2fd | |||
| 81e00993f2 | |||
| e22c7698c3 | |||
| ef053fc2f7 | |||
| 20859abda9 | |||
| 1025347c08 | |||
| 43988ea12b | |||
| 722e045b20 | |||
| ad6804019b | |||
| cd40a6e55e | |||
| 08d9e24871 | |||
| 0649678dca | |||
| a00ec2d02a | |||
| 54a74ccc2a | |||
| 6f3ab274bd | |||
| 90870ecfb6 | |||
| b640e0e181 | |||
| c41fe824cf | |||
| 62c8319d70 | |||
| b1d23467a2 | |||
| 9a62daf047 | |||
| b9ab1c6fd7 | |||
| c1054711b0 | |||
| a4d4f29a37 | |||
| 2793aadea0 | |||
| e7cfb7d9d9 | |||
| 256a851c9b | |||
| 4ab9c81cb8 | |||
| 2f13d6c89e | |||
| f1f7bad25e | |||
| a8b52a48f7 | |||
| cd0999b2ab | |||
| b4c60f3f04 | |||
| 5cefad3666 | |||
| 7a6ecd8c34 | |||
| ba96ecfd91 | |||
| 7bed3757f2 | |||
| e2e755b1d2 | |||
| a77d23109d | |||
| 0eb3b8d382 | |||
| 0021185288 | |||
| 94c72ff4f6 | |||
| e4c08dc9b2 | |||
| ec74af0d62 | |||
| b6d72dd082 | |||
| 31c311daf0 | |||
| e9b3cc6090 | |||
| 543b04a097 | |||
| d39b85b6e6 | |||
| bbe107bb2b | |||
| c616240db1 | |||
| 166be66ce3 | |||
| 4180d0a337 | |||
| 78108e316c | |||
| 3bb8b70915 | |||
| 1c4f13a0ed | |||
| 3ca0abad53 | |||
| 030c8f897f | |||
| 68175cfa3b | |||
| fd8f757851 | |||
| 876d111c61 | |||
| 730cb95c04 | |||
| 3c388d4fb5 | |||
| 2e23718d73 | |||
| 89ea8c470b | |||
| 178640bfc4 | |||
| 4ed38340e0 | |||
| 69eb9a5eff | |||
| 351425127a | |||
| 95f2ba37cc |
@@ -17,7 +17,6 @@
|
||||
# Ignore build folders
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
|
||||
# Ignore default volumes created by running docker compose up
|
||||
ontime-db
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
"ONTIME_VERSION.js"
|
||||
"ONTIME_VERSION.js"
|
||||
dist/
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
@@ -14,19 +14,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm store directory
|
||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
version: 10
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -38,16 +38,20 @@ jobs:
|
||||
|
||||
- name: Copy server
|
||||
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
|
||||
|
||||
|
||||
- name: Copy client
|
||||
run: cp -R apps/client/build apps/cli/client
|
||||
|
||||
- name: Copy external
|
||||
run: cp -R apps/server/src/external apps/cli/external
|
||||
|
||||
|
||||
- name: Setup pnpm auth config
|
||||
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
|
||||
- name: Publish to NPM
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
working-directory: ./apps/cli
|
||||
|
||||
@@ -6,43 +6,63 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
publish_docker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
- name: Get pnpm store directory
|
||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push stable release
|
||||
if: github.event.release.prerelease == false
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Build and push pre-release
|
||||
if: github.event.release.prerelease == true
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:nightly
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project packages
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: pnpm build:docker
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Build and push stable release
|
||||
if: github.event.release.prerelease == false
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||
|
||||
- name: Build and push pre-release
|
||||
if: github.event.release.prerelease == true
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:nightly
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Ontime build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ "*" ]
|
||||
tags: ['*']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -11,15 +11,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm store directory
|
||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
version: 10
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -30,7 +35,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- name: Electron - Build app
|
||||
env:
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLEID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
|
||||
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
||||
@@ -53,15 +58,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -86,15 +85,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_cli:
|
||||
build_resolver:
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
@@ -14,29 +14,33 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm store directory
|
||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
version: 10
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project packages
|
||||
run: pnpm turbo @getontime/resolver#build
|
||||
run: pnpm build:resolver
|
||||
|
||||
- name: Setup pnpm auth config
|
||||
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
|
||||
- name: Publish to NPM
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
working-directory: ./apps/resolver
|
||||
|
||||
+41
-46
@@ -14,55 +14,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm store directory
|
||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
version: 10
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Run code quality per package
|
||||
- name: React - Run linter + TypeScript checks
|
||||
if: always()
|
||||
run: pnpm lint && tsc --noEmit
|
||||
working-directory: ./apps/client
|
||||
|
||||
- name: Server - Run linter + TypeScript checks
|
||||
if: always()
|
||||
run: pnpm lint && tsc --noEmit
|
||||
working-directory: ./apps/server
|
||||
|
||||
- name: Utils - Run linter + TypeScript checks
|
||||
if: always()
|
||||
run: pnpm lint && tsc --noEmit
|
||||
working-directory: ./packages/utils
|
||||
|
||||
- name: Types - Run linter
|
||||
if: always()
|
||||
# Run code quality
|
||||
- name: Run linter
|
||||
run: pnpm lint
|
||||
working-directory: ./packages/types
|
||||
|
||||
- name: Run TypeScript checks
|
||||
run: pnpm typecheck
|
||||
|
||||
# We choose to run tests separately
|
||||
- name: React - Run unit tests
|
||||
- name: Run unit tests
|
||||
if: always()
|
||||
run: pnpm test:pipeline
|
||||
working-directory: ./apps/client
|
||||
|
||||
- name: Server - Run unit tests
|
||||
if: always()
|
||||
run: pnpm test:pipeline
|
||||
working-directory: ./apps/server
|
||||
|
||||
- name: Utils - Run unit tests
|
||||
if: always()
|
||||
run: pnpm test:pipeline
|
||||
working-directory: ./packages/utils
|
||||
run: pnpm test
|
||||
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -70,21 +50,36 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm store directory
|
||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
version: 10
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build client
|
||||
run: pnpm build:local
|
||||
- name: Get installed Playwright version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --parseable | cut -s -d '@' -f3 | cut -d '/' -f1)" >> $GITHUB_ENV
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
- run: npx playwright install --with-deps
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
+4
-1
@@ -5,7 +5,10 @@ node_modules
|
||||
|
||||
playwright-report
|
||||
|
||||
|
||||
pnpm-lock.yaml
|
||||
**/*.toml
|
||||
**/*.yml
|
||||
**/*.json
|
||||
!tsconfig.common.json
|
||||
!turbo.json
|
||||
!package.json
|
||||
|
||||
+9
-9
@@ -21,7 +21,7 @@ Locally, we would need to run both the React client and the node.js server in de
|
||||
|
||||
From the project root, run the following commands
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __Run dev mode__ by running `pnpm turbo dev`
|
||||
- __Run dev mode__ by running `pnpm dev` or `pnpm dev:electron` to get the electron window
|
||||
|
||||
|
||||
### Debugging backend
|
||||
@@ -30,10 +30,10 @@ The previous command will start the development servers for both the client, ser
|
||||
Typically in dev mode we prefer to start these in separate terminals to help with error tracking and debugging.
|
||||
|
||||
We do that by creating two terminals an running
|
||||
- __Run the React UI__ by running `pnpm turbo dev --filter=ontime-ui`
|
||||
- __Run the nodejs server__ by running `pnpm turbo dev --filter=ontime-server`
|
||||
- __Run the React UI__ by running `pnpm dev --filter=ontime-ui`
|
||||
- __Run the nodejs server__ by running `pnpm dev --filter=ontime-server`
|
||||
|
||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm turbo dev:inspect --filter=ontime-server`.
|
||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect --filter=ontime-server`.
|
||||
|
||||
## TESTING
|
||||
|
||||
@@ -46,7 +46,7 @@ Generally we have 2 types of tests.
|
||||
|
||||
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
||||
|
||||
You can run unit tests by running `pnpm turbo test:pipeline` from the project root.
|
||||
You can run unit tests by running `pnpm test:pipeline` from the project root.
|
||||
This will run all tests and close test runner.
|
||||
|
||||
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
||||
@@ -66,7 +66,7 @@ start the webserver with `pnpm dev:server`
|
||||
|
||||
Some other useful commands
|
||||
|
||||
- `pnpm e2e --ui` open playwright UI
|
||||
- `pnpm e2e:ui` open playwright UI
|
||||
- `pnpm e2e --headed` run tests with a visible browser window
|
||||
|
||||
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
||||
@@ -77,13 +77,13 @@ You can generate a distribution for your OS by running the following steps.
|
||||
From the project root, run the following commands
|
||||
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __Build the UI and server__ by running `pnpm turbo run build:electron`
|
||||
- __Create the package__ by running `pnpm turbo run dist-win`, `pnpm turbo run dist-mac` or `pnpm turbo run dist-linux`
|
||||
- __Build the UI and server__ by running `pnpm build`
|
||||
- __Create the package__ by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
||||
|
||||
The build distribution assets will be at `.apps/electron/dist`
|
||||
|
||||
Note: The MacOS build will only work in CI, locally it will fail due to notarisation issues.
|
||||
Use the `pnpm turbo run dist-mac:local` command to build a MacOS distribution locally and skip the notary process.
|
||||
Use the `pnpm dist-mac:local` command to build a MacOS distribution locally and skip the notary process.
|
||||
|
||||
## DOCKER
|
||||
|
||||
|
||||
+30
-37
@@ -1,37 +1,30 @@
|
||||
FROM node:22-bullseye AS builder
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN npm install -g pnpm@10.11.0
|
||||
COPY . /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 run build:docker
|
||||
|
||||
FROM node:22-alpine
|
||||
|
||||
# Set environment variables
|
||||
# Environment Variable to signal that we are running production
|
||||
ENV NODE_ENV=docker
|
||||
# Ontime Data path
|
||||
ENV ONTIME_DATA=/data/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
# Prepare UI
|
||||
COPY --from=builder /app/apps/client/build ./client/
|
||||
|
||||
# Prepare Backend
|
||||
COPY --from=builder /app/apps/server/dist/ ./server/
|
||||
COPY --from=builder /app/apps/server/src/external/ ./external/
|
||||
COPY --from=builder /app/apps/server/src/user/ ./user/
|
||||
COPY --from=builder /app/apps/server/src/html/ ./html/
|
||||
|
||||
# Export default ports
|
||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||
|
||||
CMD ["node", "server/docker.cjs"]
|
||||
|
||||
# Build and run commands
|
||||
# !!! Note that this command needs pre-build versions of the UI and server apps
|
||||
# docker buildx build . -t getontime/ontime
|
||||
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
|
||||
ARG NODE_VERSION=22.15.1
|
||||
FROM node:${NODE_VERSION}-alpine
|
||||
|
||||
# Set environment variables
|
||||
# Environment Variable to signal that we are running production
|
||||
ENV NODE_ENV=docker
|
||||
# Ontime Data path
|
||||
ENV ONTIME_DATA=/data/
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app/
|
||||
|
||||
# Prepare UI
|
||||
COPY apps/client/build/ ./client/
|
||||
|
||||
# Prepare Backend
|
||||
COPY apps/server/dist/ ./server/
|
||||
COPY apps/server/src/external/ ./external/
|
||||
COPY apps/server/src/user/ ./user/
|
||||
COPY apps/server/src/html/ ./html/
|
||||
|
||||
# Export default ports
|
||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||
|
||||
CMD ["node", "server/docker.cjs"]
|
||||
|
||||
# Build and run commands
|
||||
# pnpm build:docker
|
||||
# docker buildx build . -t getontime/ontime
|
||||
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "4.0.0-beta.4",
|
||||
"version": "4.0.0-beta.5",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -13,12 +13,6 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>ontime</title>
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
background-color: #101010 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "4.0.0-beta.4",
|
||||
"version": "4.0.0-beta.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@base-ui-components/react": "1.0.0-beta.3",
|
||||
"@base-ui-components/react": "1.0.0-beta.4",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -16,7 +16,7 @@
|
||||
"@tanstack/react-query-devtools": "^5.85.9",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.2",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"csv-stringify": "^6.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
@@ -37,15 +37,12 @@
|
||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||
"postinstall": "pnpm addversion",
|
||||
"dev": "cross-env BROWSER=none vite",
|
||||
"build": "vite build",
|
||||
"build:local": "cross-env NODE_ENV=local vite build",
|
||||
"build:electron": "cross-env NODE_ENV=local vite build",
|
||||
"build:docker": "cross-env VITE_IS_DOCKER=true vite build",
|
||||
"build:localdocker": "cross-env NODE_ENV=local vite build",
|
||||
"dev:electron": "pnpm dev",
|
||||
"lint": "eslint . --quiet",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "vite build",
|
||||
"test": "vitest",
|
||||
"test:pipeline": "vitest run",
|
||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build",
|
||||
"analyse": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||
|
||||
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import { createBlob, downloadBlob } from './utils';
|
||||
|
||||
@@ -33,27 +30,6 @@ export async function downloadProject(fileName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request download of the current rundown as a CSV file
|
||||
* @param fileName
|
||||
*/
|
||||
export async function downloadCSV(fileName: string = 'rundown') {
|
||||
try {
|
||||
const { data, name } = await fileDownload(fileName);
|
||||
const { project, rundowns, customFields } = data;
|
||||
|
||||
const flatRundowns = aggregateRundowns(rundowns);
|
||||
const sheetData = makeTable(project, flatRundowns, customFields);
|
||||
|
||||
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
||||
|
||||
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
||||
downloadBlob(blob, `${name}.csv`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to upload project file
|
||||
*/
|
||||
|
||||
@@ -49,15 +49,14 @@ class ErrorBoundary extends React.Component {
|
||||
return (
|
||||
<div className={style.errorContainer} data-testid='error-container'>
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
<div
|
||||
role='button'
|
||||
<p className={style.error}>: /</p>
|
||||
<p>Something went wrong.</p>
|
||||
<a
|
||||
className={style.report}
|
||||
onClick={() => Sentry.showReportDialog({ eventId: this.state.eventId })}
|
||||
href={`mailto:mail@getontime.no?subject=Error%20Report&body=${encodeURIComponent(this.reportContent)}`}
|
||||
>
|
||||
Report error
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
role='button'
|
||||
className={style.report}
|
||||
|
||||
@@ -3,24 +3,21 @@
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
background-color: $ui-black;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $error-red;
|
||||
font-weight: 600;
|
||||
}
|
||||
.error {
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.report {
|
||||
text-decoration: underline $error-red;
|
||||
cursor: pointer;
|
||||
}
|
||||
.report {
|
||||
color: $blue-500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
.report:hover {
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.report:active {
|
||||
color: white;
|
||||
&:hover {
|
||||
color: $ontime-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { IoBan } from 'react-icons/io5';
|
||||
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
@@ -10,12 +11,13 @@ interface SwatchProps {
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export default function Swatch(props: SwatchProps) {
|
||||
const { color, isSelected, onClick } = props;
|
||||
|
||||
const handleClick = () => {
|
||||
export default function Swatch({ color, isSelected, onClick }: SwatchProps) {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
onClick?.(color);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
||||
|
||||
if (!color) {
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function DelayInput(props: DelayInputProps) {
|
||||
const [value, setValue] = useState<string>('');
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// avoid wrong submit on cancel
|
||||
let ignoreChange = false;
|
||||
const ignoreChangeRef = useRef(false);
|
||||
|
||||
// set internal value on duration change
|
||||
useEffect(() => {
|
||||
@@ -35,8 +35,8 @@ export default function DelayInput(props: DelayInputProps) {
|
||||
* @param {string} newValue string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = (newValue: string) => {
|
||||
if (ignoreChange) {
|
||||
ignoreChange = false;
|
||||
if (ignoreChangeRef.current) {
|
||||
ignoreChangeRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function DelayInput(props: DelayInputProps) {
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
} else if (event.key === 'Escape') {
|
||||
ignoreChange = true;
|
||||
ignoreChangeRef.current = true;
|
||||
setValue(millisToString(duration));
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ interface AppLinkProps {
|
||||
* Component used to navigate to an editor link inside the same window
|
||||
* Handles the path to respect Ontime Clouds base URL
|
||||
*/
|
||||
export default function AppLink(props: PropsWithChildren<AppLinkProps>) {
|
||||
const { search, children } = props;
|
||||
export default function AppLink({ search, children }: PropsWithChildren<AppLinkProps>) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => navigate({ search });
|
||||
|
||||
@@ -12,9 +12,7 @@ interface ExternalLinkProps {
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
export default function ExternalLink(props: ExternalLinkProps) {
|
||||
const { href, inline, children } = props;
|
||||
|
||||
export default function ExternalLink({ href, inline, children }: ExternalLinkProps) {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
openLink(href);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
import { IoClose, IoContract, IoExpand, IoEye, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { LuCoffee } from 'react-icons/lu';
|
||||
import { useLocation } from 'react-router';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||
@@ -67,7 +68,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||
{window.isSecureContext && (
|
||||
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
||||
Keep Awake
|
||||
<IoEye />
|
||||
<LuCoffee />
|
||||
{keepAwake && <span className={style.note}>Active</span>}
|
||||
</NavigationMenuItem>
|
||||
)}
|
||||
|
||||
@@ -62,8 +62,6 @@
|
||||
padding: 2px;
|
||||
border-radius: $component-border-radius-md;
|
||||
color: $ui-white;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
border: 1px solid $gray-1000;
|
||||
|
||||
&[data-side='start'] {
|
||||
@@ -73,6 +71,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
padding-block: 0.25rem;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
max-height: var(--available-height);
|
||||
scroll-padding-block: 1.5rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
|
||||
@@ -31,14 +31,17 @@ export default function Select<T>({ options, fluid, size = 'medium', ...selectRo
|
||||
<BaseSelect.Positioner side='bottom' align='start'>
|
||||
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
||||
<BaseSelect.Popup className={styles.popup}>
|
||||
{options.map(({ disabled, label, value }) => (
|
||||
<BaseSelect.Item key={String(value)} className={styles.item} value={value} disabled={disabled}>
|
||||
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
||||
<IoCheckmark className={styles.itemIndicatorIcon} />
|
||||
</BaseSelect.ItemIndicator>
|
||||
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
|
||||
</BaseSelect.Item>
|
||||
))}
|
||||
<BaseSelect.Arrow />
|
||||
<BaseSelect.List className={styles.list}>
|
||||
{options.map(({ disabled, label, value }) => (
|
||||
<BaseSelect.Item key={String(value)} className={styles.item} value={value} disabled={disabled}>
|
||||
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
||||
<IoCheckmark className={styles.itemIndicatorIcon} />
|
||||
</BaseSelect.ItemIndicator>
|
||||
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
|
||||
</BaseSelect.Item>
|
||||
))}
|
||||
</BaseSelect.List>
|
||||
</BaseSelect.Popup>
|
||||
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
||||
</BaseSelect.Positioner>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
import { fetchCurrentRundown } from '../api/rundown';
|
||||
import { useSelectedEventId } from '../hooks/useSocket';
|
||||
import { getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||
|
||||
import useProjectData from './useProjectData';
|
||||
|
||||
@@ -54,8 +54,8 @@ export function useFlatRundown() {
|
||||
|
||||
// update data whenever the revision changes
|
||||
useEffect(() => {
|
||||
if (data.revision !== -1 && data.revision !== prevRevision) {
|
||||
const flatRundown = data.flatOrder.map((id) => data.entries[id]);
|
||||
if (data.revision !== -1 || data.revision !== prevRevision) {
|
||||
const flatRundown = data.order.map((id) => data.entries[id]);
|
||||
setFlatRundown(flatRundown);
|
||||
setPrevRevision(data.revision);
|
||||
}
|
||||
@@ -85,8 +85,8 @@ export function useFlatRundownWithMetadata() {
|
||||
/**
|
||||
* Provides access to a partial rundown based on a filter callback
|
||||
*/
|
||||
export function usePartialRundown(cb: (event: OntimeEntry) => boolean) {
|
||||
const { data, status } = useFlatRundown();
|
||||
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
||||
const { data, status } = useFlatRundownWithMetadata();
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter(cb);
|
||||
}, [data, cb]);
|
||||
|
||||
@@ -164,11 +164,6 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({
|
||||
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||
}));
|
||||
|
||||
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
offset: state.offset.absolute,
|
||||
}));
|
||||
|
||||
export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
|
||||
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||
mode: state.offset.mode,
|
||||
@@ -178,14 +173,6 @@ export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
}));
|
||||
|
||||
export const useCurrentDay = createSelector((state: RuntimeStore) => ({
|
||||
currentDay: state.eventNow?.dayOffset ?? 0,
|
||||
}));
|
||||
|
||||
export const useRuntimeOffset = createSelector((state: RuntimeStore) => ({
|
||||
offset: state.offset.absolute,
|
||||
}));
|
||||
|
||||
export const usePing = createSelector((state: RuntimeStore) => ({
|
||||
ping: state.ping,
|
||||
}));
|
||||
@@ -211,7 +198,7 @@ export const usePlayback = () => {
|
||||
|
||||
/* ======================= Overview data subscriptions ======================= */
|
||||
|
||||
export const useRundownOverview = createSelector((state: RuntimeStore) => ({
|
||||
export const useStartTimesOverview = createSelector((state: RuntimeStore) => ({
|
||||
plannedStart: state.rundown.plannedStart,
|
||||
actualStart: state.rundown.actualStart,
|
||||
plannedEnd: state.rundown.plannedEnd,
|
||||
@@ -289,11 +276,7 @@ export const useStudioTimersSocket = createSelector((state: RuntimeStore) => ({
|
||||
eventNow: state.eventNow,
|
||||
message: state.message,
|
||||
time: state.timer,
|
||||
offset: state.offset,
|
||||
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||
rundown: state.rundown,
|
||||
}));
|
||||
|
||||
export const useTimelineSocket = createSelector((state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
offset: state.offset.absolute,
|
||||
expectedRundownEnd: state.offset.expectedRundownEnd,
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { OntimeEntry, ProjectRundowns, Rundown } from 'ontime-types';
|
||||
|
||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../csv';
|
||||
import { makeCSVFromArrayOfArrays } from '../csv';
|
||||
|
||||
describe('makeCSVFromArrayOfArrays()', () => {
|
||||
it('joins an array of arrays with commas and newlines', () => {
|
||||
@@ -13,34 +11,3 @@ after newline,after comma
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateRundowns()', () => {
|
||||
it('flattens an object of rundowns into a single array', () => {
|
||||
const rundowns = {
|
||||
first: {
|
||||
id: '',
|
||||
title: '',
|
||||
revision: 0,
|
||||
order: ['1', '2'],
|
||||
flatOrder: ['1', '2'],
|
||||
entries: {
|
||||
'1': { id: '1' } as OntimeEntry,
|
||||
'2': { id: '2' } as OntimeEntry,
|
||||
},
|
||||
},
|
||||
second: {
|
||||
id: '',
|
||||
title: '',
|
||||
revision: 0,
|
||||
order: ['3', '4'],
|
||||
flatOrder: ['3', '4'],
|
||||
entries: {
|
||||
'3': { id: '3' } as OntimeEntry,
|
||||
'4': { id: '4' } as OntimeEntry,
|
||||
},
|
||||
} as Rundown,
|
||||
} as ProjectRundowns;
|
||||
|
||||
expect(aggregateRundowns(rundowns)).toStrictEqual([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||
import { OntimeEntry, ProjectRundowns } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Converts an array of arrays to a CSV file
|
||||
@@ -7,25 +6,3 @@ import { OntimeEntry, ProjectRundowns } from 'ontime-types';
|
||||
export function makeCSVFromArrayOfArrays(arrayOfArrays: string[][]): string {
|
||||
return stringify(arrayOfArrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives an object of rundowns, and flattens them into a single, linear rundown
|
||||
* Used for CSV export
|
||||
*/
|
||||
export function aggregateRundowns(rundowns: ProjectRundowns): OntimeEntry[] {
|
||||
const rundownKeys = Object.keys(rundowns);
|
||||
if (rundownKeys.length === 0) return [];
|
||||
const flatRundown: OntimeEntry[] = [];
|
||||
|
||||
for (const key of rundownKeys) {
|
||||
const { order, entries } = rundowns[key];
|
||||
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
const entryId = order[i];
|
||||
const entry = entries[entryId];
|
||||
|
||||
flatRundown.push(entry);
|
||||
}
|
||||
}
|
||||
return flatRundown;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { APP_SETTINGS } from '../api/constants';
|
||||
import { useExpectedStartData } from '../hooks/useSocket';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
import { ExtendedEntry } from './rundownMetadata';
|
||||
|
||||
/**
|
||||
* Returns current time in milliseconds from midnight
|
||||
* @returns {number}
|
||||
@@ -154,3 +156,33 @@ export function useTimeUntilExpectedStart(
|
||||
);
|
||||
return expectedStart - clock;
|
||||
}
|
||||
|
||||
export function getExpectedTimesFromExtendedEvent(
|
||||
event: Pick<
|
||||
ExtendedEntry<OntimeEvent>,
|
||||
'timeStart' | 'dayOffset' | 'delay' | 'totalGap' | 'isLinkedToLoaded' | 'countToEnd' | 'duration'
|
||||
> | null,
|
||||
state: ReturnType<typeof useExpectedStartData>,
|
||||
) {
|
||||
if (event === null) return { expectedStart: 0, timeToStart: 0, expectedEnd: 0, plannedEnd: 0 };
|
||||
|
||||
const expectedStart = getExpectedStart(
|
||||
{ timeStart: event.timeStart, delay: event.delay, dayOffset: event.dayOffset },
|
||||
{
|
||||
totalGap: event.totalGap,
|
||||
isLinkedToLoaded: event.isLinkedToLoaded,
|
||||
...state,
|
||||
},
|
||||
);
|
||||
|
||||
const plannedEnd = event.timeStart + event.duration + event.delay;
|
||||
|
||||
return {
|
||||
expectedStart,
|
||||
timeToStart: expectedStart - state.clock,
|
||||
expectedEnd: event.countToEnd
|
||||
? Math.max(expectedStart + event.duration, plannedEnd)
|
||||
: expectedStart + event.duration,
|
||||
plannedEnd,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ export const buyMeACoffeeUrl = 'https://buymeacoffee.com/cpvalente';
|
||||
|
||||
// resolve environment
|
||||
export const appVersion = version;
|
||||
export const isProduction = import.meta.env.MODE === 'production';
|
||||
export const isDev = !isProduction;
|
||||
export const isDocker = import.meta.env.IS_DOCKER; // this env is made available by the vite.config.js define function
|
||||
export const isProduction = import.meta.env.PROD;
|
||||
export const isDev = import.meta.env.DEV;
|
||||
export const currentHostName = window.location.hostname;
|
||||
export const isLocalhost = currentHostName === 'localhost' || currentHostName === '127.0.0.1';
|
||||
export const isDockerImage = Boolean(import.meta.env.VITE_IS_DOCKER);
|
||||
export const isOntimeCloud = currentHostName.includes('cloud.getontime.no');
|
||||
|
||||
// resolve entrypoint URLs
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MessageTag } from 'ontime-types';
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { usePing } from '../../../../common/hooks/useSocket';
|
||||
import { sendSocket } from '../../../../common/utils/socket';
|
||||
import { isDockerImage } from '../../../../externals';
|
||||
import { isDocker } from '../../../../externals';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Network</Panel.Header>
|
||||
{isDockerImage && (
|
||||
{isDocker && (
|
||||
<Panel.Section>
|
||||
<OntimeCloudStats />
|
||||
</Panel.Section>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
|
||||
import {
|
||||
deleteProject,
|
||||
downloadCSV,
|
||||
downloadProject,
|
||||
duplicateProject,
|
||||
loadProject,
|
||||
@@ -187,10 +186,6 @@ function ActionMenu(props: ActionMenuProps) {
|
||||
await downloadProject(filename);
|
||||
};
|
||||
|
||||
const handleExportCSV = async () => {
|
||||
await downloadCSV(filename);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
render={<IconButton variant='ghosted-white' />}
|
||||
@@ -213,7 +208,6 @@ function ActionMenu(props: ActionMenuProps) {
|
||||
{ type: 'item', icon: IoPencilOutline, label: 'Rename', onClick: handleRename },
|
||||
{ type: 'item', icon: IoCopyOutline, label: 'Duplicate', onClick: handleDuplicate },
|
||||
{ type: 'item', icon: IoDocumentOutline, label: 'Download', onClick: handleDownload },
|
||||
{ type: 'item', icon: IoDocumentOutline, label: 'Export CSV Rundown', onClick: handleExportCSV },
|
||||
{ type: 'divider' },
|
||||
{ type: 'item', icon: IoTrash, label: 'Delete', onClick: () => onDelete(filename), disabled: current },
|
||||
]}
|
||||
|
||||
@@ -31,7 +31,7 @@ const staticOptions = [
|
||||
},
|
||||
{
|
||||
id: 'manage',
|
||||
label: 'Project data',
|
||||
label: 'Project settings',
|
||||
secondary: [
|
||||
{ id: 'manage__defaults', label: 'Rundown defaults' },
|
||||
{ id: 'manage__custom', label: 'Custom fields' },
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSelectedEventId } from '../../common/hooks/useSocket';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||
import useSettings from '../../common/hooks-query/useSettings';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { throttle } from '../../common/utils/throttle';
|
||||
@@ -21,14 +21,14 @@ import OperatorGroup from './operator-group/OperatorGroup';
|
||||
import StatusBar from './status-bar/StatusBar';
|
||||
import { getOperatorOptions, useOperatorOptions } from './operator.options';
|
||||
import type { EditEvent } from './operator.types';
|
||||
import { getEventData, makeOperatorMetadata } from './operator.utils';
|
||||
import { getEventData } from './operator.utils';
|
||||
|
||||
import style from './Operator.module.scss';
|
||||
|
||||
const selectedOffset = 50;
|
||||
|
||||
export default function Operator() {
|
||||
const { data, status } = useRundown();
|
||||
const { data, rundownMetadata, status } = useRundownWithMetadata();
|
||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||
|
||||
@@ -113,7 +113,6 @@ export default function Operator() {
|
||||
}
|
||||
|
||||
const canEdit = shouldEdit && subscribe.length;
|
||||
const { process } = makeOperatorMetadata(selectedEventId);
|
||||
|
||||
return (
|
||||
<div className={style.operatorContainer} data-testid='operator-view'>
|
||||
@@ -130,8 +129,7 @@ export default function Operator() {
|
||||
{data.order.map((entryId) => {
|
||||
const entry = data.entries[entryId];
|
||||
if (isOntimeEvent(entry)) {
|
||||
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(entry);
|
||||
|
||||
const { isPast, isLinkedToLoaded, isLoaded, totalGap } = rundownMetadata[entryId];
|
||||
// hide past events (if setting) and skipped events
|
||||
if ((hidePast && isPast) || entry.skip) {
|
||||
return null;
|
||||
@@ -158,9 +156,9 @@ export default function Operator() {
|
||||
delay={entry.delay}
|
||||
dayOffset={entry.dayOffset}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
isSelected={isSelected}
|
||||
isSelected={isLoaded}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
selectedRef={isLoaded ? selectedRef : undefined}
|
||||
showStart={showStart}
|
||||
subscribed={subscribedData}
|
||||
totalGap={totalGap}
|
||||
@@ -179,7 +177,7 @@ export default function Operator() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry);
|
||||
const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[nestedEntryId];
|
||||
|
||||
// hide past events (if setting) and skipped events
|
||||
if ((hidePast && isPast) || nestedEntry.skip) {
|
||||
@@ -207,9 +205,9 @@ export default function Operator() {
|
||||
delay={nestedEntry.delay}
|
||||
dayOffset={nestedEntry.dayOffset}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
isSelected={isSelected}
|
||||
isSelected={isLoaded}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
selectedRef={isLoaded ? selectedRef : undefined}
|
||||
showStart={showStart}
|
||||
subscribed={subscribedData}
|
||||
totalGap={totalGap}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { makeOperatorMetadata } from '../operator.utils';
|
||||
|
||||
describe('makeOperatorMetadata()', () => {
|
||||
it('should track past, selected states, gaps and linking', () => {
|
||||
const event1 = { id: 'event1', gap: 5, linkStart: false } as OntimeEvent;
|
||||
const event2 = { id: 'event2', gap: 10, linkStart: true } as OntimeEvent;
|
||||
const event3 = { id: 'event3', gap: 15, linkStart: true } as OntimeEvent;
|
||||
const { process } = makeOperatorMetadata('event2');
|
||||
|
||||
expect(process(event1)).toEqual({
|
||||
isPast: true,
|
||||
isSelected: false,
|
||||
totalGap: 5,
|
||||
isLinkedToLoaded: false,
|
||||
});
|
||||
expect(process(event2)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: true,
|
||||
totalGap: 15,
|
||||
isLinkedToLoaded: false,
|
||||
});
|
||||
expect(process(event3)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: false,
|
||||
totalGap: 30,
|
||||
isLinkedToLoaded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null selectedId', () => {
|
||||
const event1 = { id: 'event1', gap: 5, linkStart: true } as OntimeEvent;
|
||||
const event2 = { id: 'event2', gap: 10, linkStart: false } as OntimeEvent;
|
||||
const event3 = { id: 'event3', gap: 15, linkStart: true } as OntimeEvent;
|
||||
const { process } = makeOperatorMetadata(null);
|
||||
|
||||
expect(process(event1)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: false,
|
||||
totalGap: 5,
|
||||
isLinkedToLoaded: true,
|
||||
});
|
||||
expect(process(event2)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: false,
|
||||
totalGap: 15,
|
||||
isLinkedToLoaded: false,
|
||||
});
|
||||
expect(process(event3)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: false,
|
||||
totalGap: 30,
|
||||
isLinkedToLoaded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should break linking chain on countToEnd events', () => {
|
||||
const event1 = { id: 'event1', gap: 5, linkStart: true, countToEnd: false } as OntimeEvent;
|
||||
const event2 = { id: 'event2', gap: 10, linkStart: true, countToEnd: true } as OntimeEvent;
|
||||
const event3 = { id: 'event3', gap: 15, linkStart: true, countToEnd: false } as OntimeEvent;
|
||||
const { process } = makeOperatorMetadata(null);
|
||||
|
||||
expect(process(event1)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: false,
|
||||
totalGap: 5,
|
||||
isLinkedToLoaded: true,
|
||||
});
|
||||
expect(process(event2)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: false,
|
||||
totalGap: 15,
|
||||
isLinkedToLoaded: true,
|
||||
});
|
||||
expect(process(event3)).toEqual({
|
||||
isPast: false,
|
||||
isSelected: false,
|
||||
totalGap: 30,
|
||||
isLinkedToLoaded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -71,7 +71,7 @@ function OperatorEvent({
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={operatorClasses} ref={selectedRef} onContextMenu={handleLongPress} {...mouseHandlers}>
|
||||
<div className={operatorClasses} data-testid={cue} ref={selectedRef} onContextMenu={handleLongPress} {...mouseHandlers}>
|
||||
<div className={style.binder} style={{ ...cueColours }}>
|
||||
<span className={style.cue}>{cue}</span>
|
||||
</div>
|
||||
@@ -167,5 +167,5 @@ function TimeUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }:
|
||||
const isDue = timeUntil < MILLIS_PER_SECOND;
|
||||
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`;
|
||||
|
||||
return <span className={style.timeUntil}>{timeUntilString}</span>;
|
||||
return <span className={style.timeUntil} data-testid='time-until'>{timeUntilString}</span>;
|
||||
}
|
||||
|
||||
@@ -1,74 +1,33 @@
|
||||
import { CustomFields, EntryId, MaybeString, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { getPropertyValue } from '../viewers/common/viewUtils';
|
||||
|
||||
import type { Subscribed } from './operator.types';
|
||||
|
||||
type OperatorMetadata = {
|
||||
isLinkedToLoaded: boolean;
|
||||
isPast: boolean;
|
||||
isSelected: boolean;
|
||||
totalGap: number;
|
||||
};
|
||||
|
||||
export function makeOperatorMetadata(selectedId: EntryId | null) {
|
||||
const hasSelection = Boolean(selectedId);
|
||||
let hasSeenSelected = false;
|
||||
let totalGap = 0;
|
||||
/** if the event can link all the way back to the currently playing event */
|
||||
let isLinkedToLoaded = false;
|
||||
let previousEvent: OntimeEvent | null = null;
|
||||
|
||||
function process(event: OntimeEvent): Readonly<OperatorMetadata> {
|
||||
const isSelected = event.id === selectedId;
|
||||
if (isSelected) {
|
||||
hasSeenSelected = true;
|
||||
}
|
||||
|
||||
// is past if we havent yet seen the selected event
|
||||
const isPast = hasSelection && !hasSeenSelected;
|
||||
totalGap += event.gap;
|
||||
|
||||
if (!isPast && !isSelected) {
|
||||
/**
|
||||
* isLinkToLoaded is a chain value that we maintain until we
|
||||
* a) find an unlinked event
|
||||
* b) find a countToEnd event
|
||||
*/
|
||||
isLinkedToLoaded = event.linkStart && !previousEvent?.countToEnd;
|
||||
}
|
||||
|
||||
previousEvent = event;
|
||||
return { isPast, isSelected, totalGap, isLinkedToLoaded };
|
||||
}
|
||||
|
||||
return { process };
|
||||
}
|
||||
|
||||
export function getEventData(
|
||||
event: OntimeEvent,
|
||||
main: MaybeString,
|
||||
secondary: MaybeString,
|
||||
subscriptions: string[],
|
||||
customFields: CustomFields,
|
||||
) {
|
||||
const mainField = main ? getPropertyValue(event, main) ?? '' : event.title;
|
||||
const secondaryField = getPropertyValue(event, secondary) ?? '';
|
||||
|
||||
// remove subscriptions that are not in customFields
|
||||
const sanitisedSubscriptions = subscriptions.filter((field) => Object.hasOwn(customFields, field));
|
||||
const subscribedData = sanitisedSubscriptions.reduce<Subscribed>((acc, id) => {
|
||||
const field = customFields[id];
|
||||
if (field) {
|
||||
acc.push({
|
||||
id,
|
||||
label: field.label,
|
||||
colour: field.colour,
|
||||
value: event.custom[id],
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return { mainField, secondaryField, subscribedData };
|
||||
}
|
||||
import { CustomFields, MaybeString, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { getPropertyValue } from '../viewers/common/viewUtils';
|
||||
|
||||
import type { Subscribed } from './operator.types';
|
||||
|
||||
export function getEventData(
|
||||
event: OntimeEvent,
|
||||
main: MaybeString,
|
||||
secondary: MaybeString,
|
||||
subscriptions: string[],
|
||||
customFields: CustomFields,
|
||||
) {
|
||||
const mainField = main ? getPropertyValue(event, main) ?? '' : event.title;
|
||||
const secondaryField = getPropertyValue(event, secondary) ?? '';
|
||||
|
||||
// remove subscriptions that are not in customFields
|
||||
const sanitisedSubscriptions = subscriptions.filter((field) => Object.hasOwn(customFields, field));
|
||||
const subscribedData = sanitisedSubscriptions.reduce<Subscribed>((acc, id) => {
|
||||
const field = customFields[id];
|
||||
if (field) {
|
||||
acc.push({
|
||||
id,
|
||||
label: field.label,
|
||||
colour: field.colour,
|
||||
value: event.custom[id],
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return { mainField, secondaryField, subscribedData };
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
useNextFlag,
|
||||
useOffsetOverview,
|
||||
useProgressOverview,
|
||||
useRundownOverview,
|
||||
useStartTimesOverview,
|
||||
useTimer,
|
||||
} from '../../../common/hooks/useSocket';
|
||||
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||
@@ -34,7 +34,7 @@ import { OverUnder, TimeColumn } from './TimeLayout';
|
||||
import style from './TimeElements.module.scss';
|
||||
|
||||
export function StartTimes() {
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRundownOverview();
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useStartTimesOverview();
|
||||
|
||||
const plannedStartText = plannedStart === null ? timerPlaceholder : formatTime(plannedStart);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function StartTimes() {
|
||||
<Tooltip text='Planned start time' render={<TbCalendarPin className={style.icon} />} />
|
||||
<span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<div className={style.labelledElement} data-testid='actual-start-time'>
|
||||
<Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} />
|
||||
<span className={cx([style.time, actualStart === null && style.muted])}>{formattedTime(actualStart)}</span>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,6 @@ export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
|
||||
|
||||
const dragStyle = {
|
||||
zIndex: isDragging ? 2 : 'inherit',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
@@ -205,7 +205,6 @@ export default function RundownEvent({
|
||||
|
||||
const dragStyle = {
|
||||
zIndex: isDragging ? 2 : 'inherit',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
||||
zIndex: isDragging ? 2 : 'inherit',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'grab',
|
||||
cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'inherit',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -70,7 +70,6 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
||||
|
||||
const dragStyle = {
|
||||
zIndex: isDragging ? 2 : 'inherit',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
@@ -225,6 +225,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
<Switch
|
||||
size='large'
|
||||
name='lockNav'
|
||||
data-testid='lockNav'
|
||||
checked={watch('lockNav')}
|
||||
onCheckedChange={(checked) => setValue('lockNav', checked, { shouldDirty: true })}
|
||||
disabled={watch('lockConfig')}
|
||||
@@ -239,6 +240,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
<Switch
|
||||
size='large'
|
||||
name='lockConfig'
|
||||
data-testid='lockConfig'
|
||||
checked={watch('lockConfig')}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
@@ -254,6 +256,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
<Switch
|
||||
size='large'
|
||||
name='authenticate'
|
||||
data-testid='authenticate'
|
||||
checked={watch('authenticate')}
|
||||
onCheckedChange={(checked) => setValue('authenticate', checked, { shouldDirty: true })}
|
||||
/>
|
||||
|
||||
@@ -63,7 +63,6 @@ body,
|
||||
html {
|
||||
font-size: 15px;
|
||||
font-family: $ontime-font-family;
|
||||
background-color: var(--background-color-override, $ui-black);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -42,4 +42,4 @@ $timer-bold-font-family: 'Arial Black', sans-serif; // --font-family-bold-overri
|
||||
$external-color: rgba(white, 85%); // --external-color-override
|
||||
|
||||
// properties related to the studio clock
|
||||
$studio-breakpoint: 1200px;
|
||||
$studio-breakpoint: 1300px;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
import { getScheduledTimes } from './schedule.utils';
|
||||
import { useSchedule } from './ScheduleContext';
|
||||
import ScheduleItem from './ScheduleItem';
|
||||
|
||||
@@ -20,17 +19,21 @@ export default function Schedule({ className }: ScheduleProps) {
|
||||
return (
|
||||
<ul className={cx(['schedule', className])} ref={containerRef}>
|
||||
{events.map((event) => {
|
||||
const { timeStart, timeEnd, delay } = getScheduledTimes(event);
|
||||
|
||||
return (
|
||||
<ScheduleItem
|
||||
key={event.id}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
title={event.title}
|
||||
timeStart={event.timeStart}
|
||||
dayOffset={event.dayOffset}
|
||||
delay={event.delay}
|
||||
totalGap={event.totalGap}
|
||||
isLinkedToLoaded={event.isLinkedToLoaded}
|
||||
countToEnd={event.countToEnd}
|
||||
duration={event.duration}
|
||||
colour={event.colour}
|
||||
skip={event.skip}
|
||||
delay={delay}
|
||||
title={event.title}
|
||||
timeEnd={event.timeEnd}
|
||||
cue={event.cue}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2,11 +2,12 @@ import { createContext, PropsWithChildren, RefObject, use, useEffect, useLayoutE
|
||||
import { EntryId, isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
||||
import { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||
|
||||
import { useScheduleOptions } from './schedule.options';
|
||||
|
||||
interface ScheduleContextState {
|
||||
events: OntimeEvent[];
|
||||
events: ExtendedEntry<OntimeEvent>[];
|
||||
selectedEventId: string | null;
|
||||
numPages: number;
|
||||
visiblePage: number;
|
||||
@@ -21,7 +22,7 @@ interface ScheduleProviderProps {
|
||||
|
||||
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
|
||||
const { cycleInterval, stopCycle, filter } = useScheduleOptions();
|
||||
const { data: events } = usePartialRundown((entry: OntimeEntry) => {
|
||||
const { data: events } = usePartialRundown((entry: ExtendedEntry<OntimeEntry>) => {
|
||||
if (filter) {
|
||||
// custom keys are prepended with custom-
|
||||
const customKey = filter.startsWith('custom-') ? filter.slice('custom-'.length) : filter;
|
||||
@@ -133,7 +134,7 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
|
||||
return (
|
||||
<ScheduleContext
|
||||
value={{
|
||||
events: viewEvents as OntimeEvent[],
|
||||
events: viewEvents as ExtendedEntry<OntimeEvent>[],
|
||||
selectedEventId,
|
||||
numPages,
|
||||
visiblePage,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useRuntimeOffset } from '../../../common/hooks/useSocket';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { useExpectedStartData } from '../../../common/hooks/useSocket';
|
||||
import { getOffsetState } from '../../../common/utils/offset';
|
||||
import { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { formatTime, getExpectedTimesFromExtendedEvent } from '../../../common/utils/time';
|
||||
import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||
|
||||
import { useScheduleOptions } from './schedule.options';
|
||||
@@ -13,110 +16,150 @@ const formatOptions = {
|
||||
format24: 'HH:mm',
|
||||
};
|
||||
|
||||
interface ScheduleItemProps {
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
title: string;
|
||||
colour?: string;
|
||||
skip?: boolean;
|
||||
delay: number;
|
||||
}
|
||||
type ScheduleItemProps = Pick<
|
||||
ExtendedEntry<OntimeEvent>,
|
||||
| 'timeStart'
|
||||
| 'dayOffset'
|
||||
| 'delay'
|
||||
| 'totalGap'
|
||||
| 'isLinkedToLoaded'
|
||||
| 'countToEnd'
|
||||
| 'duration'
|
||||
| 'colour'
|
||||
| 'skip'
|
||||
| 'title'
|
||||
| 'timeEnd'
|
||||
| 'cue'
|
||||
>;
|
||||
|
||||
export default function ScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
|
||||
export default function ScheduleItem({
|
||||
timeStart,
|
||||
dayOffset,
|
||||
delay,
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
countToEnd,
|
||||
colour,
|
||||
duration,
|
||||
skip,
|
||||
title,
|
||||
timeEnd,
|
||||
cue,
|
||||
}: ScheduleItemProps) {
|
||||
const { showExpected } = useScheduleOptions();
|
||||
|
||||
if (showExpected) {
|
||||
return (
|
||||
<ExpectedScheduleItem
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
title={title}
|
||||
colour={colour}
|
||||
skip={skip}
|
||||
delay={delay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (delay > 0) {
|
||||
return (
|
||||
<DelayedScheduleItem
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
title={title}
|
||||
colour={colour}
|
||||
skip={skip}
|
||||
delay={delay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const start = formatTime(timeStart, formatOptions);
|
||||
const end = formatTime(timeEnd, formatOptions);
|
||||
return (
|
||||
<li className={cx(['entry', skip && 'entry--skip'])}>
|
||||
<li className={cx(['entry', skip && 'entry--skip'])} data-testid={cue}>
|
||||
<div className='entry-times'>
|
||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||
<SuperscriptTime time={start} />
|
||||
→
|
||||
<SuperscriptTime time={end} />
|
||||
{showExpected ? (
|
||||
<ExpectedScheduleItem
|
||||
timeStart={timeStart}
|
||||
dayOffset={dayOffset}
|
||||
delay={delay}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
countToEnd={countToEnd}
|
||||
duration={duration}
|
||||
colour={colour}
|
||||
/>
|
||||
) : delay > 0 ? (
|
||||
<DelayedScheduleItem timeStart={timeStart} delay={delay} colour={colour} timeEnd={timeEnd} />
|
||||
) : (
|
||||
<PlannedScheduleItem timeStart={timeStart} timeEnd={timeEnd} colour={colour} />
|
||||
)}
|
||||
</div>
|
||||
<div className='entry-title'>{title}</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function DelayedScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
|
||||
function PlannedScheduleItem({
|
||||
timeStart,
|
||||
timeEnd,
|
||||
colour,
|
||||
}: Pick<ScheduleItemProps, 'timeStart' | 'timeEnd' | 'colour'>) {
|
||||
const start = formatTime(timeStart, formatOptions);
|
||||
const end = formatTime(timeEnd, formatOptions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||
<SuperscriptTime time={start} />
|
||||
→
|
||||
<SuperscriptTime time={end} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DelayedScheduleItem({
|
||||
timeStart,
|
||||
timeEnd,
|
||||
colour,
|
||||
delay,
|
||||
}: Pick<ScheduleItemProps, 'timeStart' | 'timeEnd' | 'colour' | 'delay'>) {
|
||||
const start = formatTime(timeStart, formatOptions);
|
||||
const end = formatTime(timeEnd, formatOptions);
|
||||
const delayedStart = formatTime(timeStart + delay, formatOptions);
|
||||
const delayedEnd = formatTime(timeEnd + delay, formatOptions);
|
||||
|
||||
return (
|
||||
<li className={cx(['entry', skip && 'entry--skip'])}>
|
||||
<div className='entry-times'>
|
||||
<span className='entry-times--delayed'>
|
||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||
<SuperscriptTime time={start} />
|
||||
→
|
||||
<SuperscriptTime time={end} />
|
||||
</span>
|
||||
<span className='entry-times--delay'>
|
||||
<SuperscriptTime time={delayedStart} />
|
||||
→
|
||||
<SuperscriptTime time={delayedEnd} />
|
||||
</span>
|
||||
</div>
|
||||
<div className='entry-title'>{title}</div>
|
||||
</li>
|
||||
<>
|
||||
<span className='entry-times--delayed'>
|
||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||
<SuperscriptTime time={start} />
|
||||
→
|
||||
<SuperscriptTime time={end} />
|
||||
</span>
|
||||
<span className='entry-times--delay'>
|
||||
<SuperscriptTime time={delayedStart} />
|
||||
→
|
||||
<SuperscriptTime time={delayedEnd} />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpectedScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
|
||||
function ExpectedScheduleItem({
|
||||
timeStart,
|
||||
dayOffset,
|
||||
delay,
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
countToEnd,
|
||||
colour,
|
||||
duration,
|
||||
}: Omit<ScheduleItemProps, 'timeEnd' | 'cue' | 'skip' | 'title'>) {
|
||||
const expectedStartData = useExpectedStartData();
|
||||
const { expectedStart, expectedEnd, plannedEnd } = getExpectedTimesFromExtendedEvent(
|
||||
{
|
||||
timeStart,
|
||||
dayOffset,
|
||||
delay,
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
countToEnd,
|
||||
duration,
|
||||
},
|
||||
expectedStartData,
|
||||
);
|
||||
|
||||
return (
|
||||
<li className={cx(['entry', skip && 'entry--skip'])}>
|
||||
<div className='entry-times'>
|
||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||
<ExpectedTime time={timeStart} delay={delay} />
|
||||
→
|
||||
<ExpectedTime time={timeEnd} delay={delay} />
|
||||
</div>
|
||||
<div className='entry-title'>{title}</div>
|
||||
</li>
|
||||
<>
|
||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||
<ExpectedTime expectedTime={expectedStart} plannedTime={timeStart} />
|
||||
→
|
||||
<ExpectedTime expectedTime={expectedEnd} plannedTime={plannedEnd} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExpectedTimeProps {
|
||||
time: number;
|
||||
delay: number;
|
||||
expectedTime: number;
|
||||
plannedTime: number;
|
||||
}
|
||||
|
||||
function ExpectedTime({ time, delay }: ExpectedTimeProps) {
|
||||
const { offset } = useRuntimeOffset();
|
||||
|
||||
const expectedOffset = offset - delay;
|
||||
const expectedTime = formatTime(time + offset, formatOptions);
|
||||
const expectedState = getOffsetState(expectedOffset);
|
||||
|
||||
return <SuperscriptTime className={`entry-times--${expectedState}`} time={expectedTime} />;
|
||||
function ExpectedTime({ expectedTime, plannedTime }: ExpectedTimeProps) {
|
||||
const timeDisplay = formatTime(expectedTime);
|
||||
const expectedState = getOffsetState(expectedTime - plannedTime);
|
||||
return <SuperscriptTime className={`entry-times--${expectedState}`} time={timeDisplay} />;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Gather rules for how to present scheduled times
|
||||
*/
|
||||
export function getScheduledTimes(event: OntimeEvent) {
|
||||
return {
|
||||
timeStart: event.timeStart,
|
||||
timeEnd: event.timeEnd,
|
||||
delay: event.skip ? 0 : event.delay,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { IoArrowBack, IoClose, IoSaveOutline } from 'react-icons/io5';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { EntryId, OntimeEvent } from 'ontime-types';
|
||||
import { EntryId, PlayableEvent } from 'ontime-types';
|
||||
|
||||
import Button from '../../common/components/buttons/Button';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
@@ -12,7 +12,7 @@ import { makeSubscriptionsUrl } from './countdown.utils';
|
||||
import './Countdown.scss';
|
||||
|
||||
interface CountdownSelectProps {
|
||||
events: OntimeEvent[];
|
||||
events: PlayableEvent[];
|
||||
subscriptions: EntryId[];
|
||||
disableEdit: () => void;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
|
||||
|
||||
return (
|
||||
<div className='list-container'>
|
||||
{events.map((event: OntimeEvent, index: number) => {
|
||||
{events.map((event, index) => {
|
||||
const title = event.title || '{no title}';
|
||||
const isSelected = selectedIds.has(event.id);
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
|
||||
key={event.id}
|
||||
ref={isLive ? selectedRef : undefined}
|
||||
className={cx(['sub', isLive && 'sub--live', isArmed && 'sub--armed'])}
|
||||
data-testid={event.cue}
|
||||
>
|
||||
<div className='sub__binder' style={{ '--user-color': event.colour }} />
|
||||
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { ProjectData } from 'ontime-types';
|
||||
|
||||
import { makeTable, parseField } from '../cuesheet.utils';
|
||||
import { parseField } from '../cuesheet.utils';
|
||||
|
||||
describe('parseField()', () => {
|
||||
it('returns a string from given millis on timeStart, TimeEnd and duration', () => {
|
||||
@@ -32,69 +30,3 @@ describe('parseField()', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeTable()', () => {
|
||||
it('returns array of arrays with given fields', () => {
|
||||
const headerData = {
|
||||
title: 'test title',
|
||||
description: 'test description',
|
||||
logo: 'test logo',
|
||||
};
|
||||
const tableData = [
|
||||
{
|
||||
title: 'test title 1',
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
skip: true,
|
||||
lighting: { value: 'test lighting' },
|
||||
sound: { value: 'test sound' },
|
||||
},
|
||||
];
|
||||
const customFields = {
|
||||
lighting: { label: 'test' },
|
||||
};
|
||||
|
||||
// @ts-expect-error -- testing user data with missing fields
|
||||
const table = makeTable(headerData as ProjectData, tableData, customFields);
|
||||
expect(table).not.toContain('test logo');
|
||||
expect(table).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
"Ontime · Rundown export",
|
||||
],
|
||||
[
|
||||
"Project title: test title",
|
||||
],
|
||||
[
|
||||
"Project description: test description",
|
||||
],
|
||||
[
|
||||
"Time Start",
|
||||
"Time End",
|
||||
"Duration",
|
||||
"ID",
|
||||
"Colour",
|
||||
"Cue",
|
||||
"Title",
|
||||
"Note",
|
||||
"Skip?",
|
||||
"lighting",
|
||||
"Type",
|
||||
],
|
||||
[
|
||||
"00:00:00",
|
||||
"00:00:00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"test title 1",
|
||||
"",
|
||||
"x",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ export function SortableCell({ columnId, colSpan, injectedStyles, children, drag
|
||||
const dragStyle = {
|
||||
...injectedStyles,
|
||||
zIndex: isDragging ? 2 : 'inherit',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
CustomFields,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
MaybeNumber,
|
||||
OntimeEntry,
|
||||
OntimeEntryCommonKeys,
|
||||
ProjectData,
|
||||
} from 'ontime-types';
|
||||
import { CustomFields, MaybeNumber, OntimeEntryCommonKeys } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
type CsvHeaderKey = OntimeEntryCommonKeys | keyof CustomFields;
|
||||
@@ -29,72 +21,3 @@ export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
||||
|
||||
return String(data ?? '');
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Creates an array of arrays usable by xlsx for export
|
||||
*/
|
||||
export const makeTable = (headerData: ProjectData, rundown: OntimeEntry[], customFields: CustomFields): string[][] => {
|
||||
// create metadata header row
|
||||
const data = [['Ontime · Rundown export']];
|
||||
if (headerData.title) data.push([`Project title: ${headerData.title}`]);
|
||||
if (headerData.description) data.push([`Project description: ${headerData.description}`]);
|
||||
|
||||
const customFieldKeys = Object.keys(customFields).map((key) => `custom-${key}`);
|
||||
const customFieldLabels = Object.keys(customFields);
|
||||
|
||||
// we chose not to expose internals of the application
|
||||
const fieldOrder: CsvHeaderKey[] = [
|
||||
'timeStart',
|
||||
'timeEnd',
|
||||
'duration',
|
||||
'id',
|
||||
'colour',
|
||||
'cue',
|
||||
'title',
|
||||
'note',
|
||||
'skip',
|
||||
...customFieldKeys,
|
||||
'type',
|
||||
];
|
||||
|
||||
const fieldTitles = [
|
||||
'Time Start',
|
||||
'Time End',
|
||||
'Duration',
|
||||
'ID',
|
||||
'Colour',
|
||||
'Cue',
|
||||
'Title',
|
||||
'Note',
|
||||
'Skip?',
|
||||
...customFieldLabels,
|
||||
'Type',
|
||||
];
|
||||
|
||||
// add header row to data
|
||||
data.push(fieldTitles);
|
||||
rundown.forEach((entry) => {
|
||||
if (isOntimeDelay(entry)) return;
|
||||
|
||||
const row: string[] = [];
|
||||
fieldOrder.forEach((field) => {
|
||||
if (isOntimeEvent(entry)) {
|
||||
// for custom fields, we need to extract the value from the custom object
|
||||
if (field.startsWith('custom-')) {
|
||||
const fieldLabel = field.split('custom-')[1];
|
||||
const value = entry.custom[fieldLabel];
|
||||
row.push(parseField(fieldLabel, value));
|
||||
} else {
|
||||
// @ts-expect-error -- it is ok, we will just not have the data for other fields
|
||||
row.push(parseField(field, entry[field]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error -- it is ok, we will just not have the data for other fields
|
||||
row.push(parseField(field, entry[field]));
|
||||
});
|
||||
data.push(row);
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Playback } from 'ontime-types';
|
||||
|
||||
import { useIsMobileDevice } from '../../common/hooks/useIsMobileDevice';
|
||||
import { useIsSmallScreen } from '../../common/hooks/useIsSmallScreen';
|
||||
import { useStudioClockSocket } from '../../common/hooks/useSocket';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
@@ -18,12 +18,12 @@ interface StudioClockProps {
|
||||
}
|
||||
|
||||
export default function StudioClock({ hideCards }: StudioClockProps) {
|
||||
const isMobile = useIsMobileDevice();
|
||||
const isSmallScreen = useIsSmallScreen();
|
||||
const { clock, playback } = useStudioClockSocket();
|
||||
const onAir = playback !== Playback.Stop;
|
||||
|
||||
// if we are on mobile and have to show the cards
|
||||
if (isMobile && !hideCards) {
|
||||
if (isSmallScreen && !hideCards) {
|
||||
return <StudioClockMobile clock={clock} onAir={onAir} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $view-element-gap;
|
||||
margin-top: 5%;
|
||||
margin-block: auto;
|
||||
font-size: $base-font-size;
|
||||
|
||||
.card {
|
||||
|
||||
@@ -17,17 +17,17 @@ interface StudioTimersProps {
|
||||
|
||||
export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const { eventNow, eventNext, message, time, offset, rundown } = useStudioTimersSocket();
|
||||
const { eventNow, eventNext, message, time, offset, rundown, expectedRundownEnd } = useStudioTimersSocket();
|
||||
|
||||
const schedule = getFormattedScheduleTimes({
|
||||
offset: offset.absolute,
|
||||
offset: offset,
|
||||
actualStart: rundown.actualStart,
|
||||
expectedEnd: offset.expectedRundownEnd,
|
||||
expectedEnd: expectedRundownEnd,
|
||||
});
|
||||
const event = getFormattedEventData(eventNow, time);
|
||||
const eventNextTitle = eventNext?.title || '-';
|
||||
const formattedTimerMessage = (message.timer.visible && message.timer.text) || '-';
|
||||
const formattedSecondaryMessage = message.secondary || '-';
|
||||
const formattedSecondaryMessage = message.timer.secondarySource === 'secondary' ? message.secondary || '-' : '-';
|
||||
|
||||
// gather presentation styles
|
||||
const timerColour = getTimerColour(
|
||||
@@ -37,7 +37,7 @@ export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
||||
time.phase === TimerPhase.Danger,
|
||||
);
|
||||
|
||||
const offsetState = getOffsetState(offset.absolute);
|
||||
const offsetState = getOffsetState(offset);
|
||||
|
||||
return (
|
||||
<div className='studio__timers'>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isOntimeEvent, isPlayableEvent, OntimeEntry, PlayableEvent } from 'onti
|
||||
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent';
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
|
||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||
@@ -15,7 +16,7 @@ import style from './Timeline.module.scss';
|
||||
|
||||
interface TimelineProps {
|
||||
firstStart: number;
|
||||
rundown: OntimeEntry[];
|
||||
rundown: ExtendedEntry<OntimeEntry>[];
|
||||
selectedEventId: string | null;
|
||||
totalDuration: number;
|
||||
}
|
||||
@@ -45,7 +46,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
|
||||
|
||||
const { positions, totalWidth } = useMemo(() => {
|
||||
const playableEvents = rundown
|
||||
.filter((event): event is PlayableEvent => isOntimeEvent(event) && isPlayableEvent(event))
|
||||
.filter((event): event is ExtendedEntry<PlayableEvent> => isOntimeEvent(event) && isPlayableEvent(event))
|
||||
.map((event) => ({
|
||||
start: event.timeStart + (event.dayOffset ?? 0) * dayInMs + (event.delay ?? 0),
|
||||
duration: event.duration,
|
||||
@@ -96,7 +97,11 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
|
||||
left={position.left}
|
||||
status={statusMap[event.id]}
|
||||
start={event.timeStart + (event.dayOffset ?? 0) * dayInMs}
|
||||
totalGap={event.totalGap}
|
||||
isLinkedToLoaded={event.isLinkedToLoaded}
|
||||
dayOffset={event.dayOffset}
|
||||
title={event.title}
|
||||
cue={event.cue}
|
||||
width={position.width}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { RefObject } from 'react';
|
||||
|
||||
import { useTimelineStatus, useTimer } from '../../common/hooks/useSocket';
|
||||
import { useExpectedStartData, useTimer } from '../../common/hooks/useSocket';
|
||||
import { getProgress } from '../../common/utils/getProgress';
|
||||
import { alpha, cx } from '../../common/utils/styleUtils';
|
||||
import { formatDuration, formatTime } from '../../common/utils/time';
|
||||
import { formatDuration, formatTime, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
|
||||
import { getStatusLabel, getTimeToStart } from './timeline.utils';
|
||||
import { getStatusLabel } from './timeline.utils';
|
||||
|
||||
import style from './Timeline.module.scss';
|
||||
|
||||
@@ -20,8 +20,12 @@ interface TimelineEntryProps {
|
||||
left: number;
|
||||
status: ProgressStatus;
|
||||
start: number;
|
||||
dayOffset: number;
|
||||
totalGap: number;
|
||||
isLinkedToLoaded: boolean;
|
||||
title: string;
|
||||
width: number;
|
||||
cue: string;
|
||||
ref?: RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
@@ -38,8 +42,12 @@ export function TimelineEntry({
|
||||
left,
|
||||
status,
|
||||
start,
|
||||
dayOffset,
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
title,
|
||||
width,
|
||||
cue,
|
||||
ref,
|
||||
}: TimelineEntryProps) {
|
||||
const formattedStartTime = formatTime(start, formatOptions);
|
||||
@@ -61,6 +69,7 @@ export function TimelineEntry({
|
||||
left: `${left}px`,
|
||||
width: `${width}px`,
|
||||
}}
|
||||
data-testid={cue}
|
||||
>
|
||||
{status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
|
||||
<div
|
||||
@@ -73,11 +82,29 @@ export function TimelineEntry({
|
||||
<div className={style.maybeInline}>
|
||||
<div className={cx([hasDelay && style.cross])}>{formattedStartTime}</div>
|
||||
{hasDelay && <div className={style.delay}>{formatTime(delayedStart, formatOptions)}</div>}
|
||||
{smallArea && <TimelineEntryStatus delay={delay} start={start} status={status} />}
|
||||
{smallArea && (
|
||||
<TimelineEntryStatus
|
||||
delay={delay}
|
||||
start={start}
|
||||
dayOffset={dayOffset}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showTitle && (
|
||||
<>
|
||||
{!smallArea && <TimelineEntryStatus delay={delay} start={start} status={status} />}
|
||||
{!smallArea && (
|
||||
<TimelineEntryStatus
|
||||
delay={delay}
|
||||
start={start}
|
||||
dayOffset={dayOffset}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
<div>{title}</div>
|
||||
</>
|
||||
)}
|
||||
@@ -92,16 +119,31 @@ export function TimelineEntry({
|
||||
interface TimelineEntryStatusProps {
|
||||
delay: number;
|
||||
start: number;
|
||||
dayOffset: number;
|
||||
totalGap: number;
|
||||
isLinkedToLoaded: boolean;
|
||||
status: ProgressStatus;
|
||||
}
|
||||
|
||||
// extract component to isolate re-renders provoked by the clock changes
|
||||
function TimelineEntryStatus({ delay, start, status }: TimelineEntryStatusProps) {
|
||||
const { clock, offset } = useTimelineStatus();
|
||||
function TimelineEntryStatus({
|
||||
delay,
|
||||
start,
|
||||
dayOffset,
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
status,
|
||||
}: TimelineEntryStatusProps) {
|
||||
const state = useExpectedStartData();
|
||||
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
// start times need to be normalised in a rundown that crosses midnight
|
||||
let statusText = getStatusLabel(getTimeToStart(clock, start, delay, offset), status);
|
||||
const { timeToStart } = getExpectedTimesFromExtendedEvent(
|
||||
{ timeStart: start, delay, dayOffset, totalGap, isLinkedToLoaded, countToEnd: false, duration: 0 },
|
||||
state,
|
||||
);
|
||||
|
||||
let statusText = getStatusLabel(timeToStart, status);
|
||||
if (statusText === 'live') {
|
||||
statusText = getLocalizedString('timeline.live');
|
||||
} else if (statusText === 'pending') {
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
.section-content--next {
|
||||
.section-content--followedBy .section-content--next {
|
||||
color: $green-500;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { useTimelineSocket } from '../../common/hooks/useSocket';
|
||||
import { formatDuration } from '../../common/utils/time';
|
||||
import { useExpectedStartData } from '../../common/hooks/useSocket';
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { formatDuration, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
|
||||
import TimelineSection from './timeline-section/TimelineSection';
|
||||
import { getTimeToStart } from './timeline.utils';
|
||||
|
||||
interface TimelineSectionsProps {
|
||||
now: OntimeEvent | null;
|
||||
next: OntimeEvent | null;
|
||||
followedBy: OntimeEvent | null;
|
||||
now: ExtendedEntry<OntimeEvent> | null;
|
||||
next: ExtendedEntry<OntimeEvent> | null;
|
||||
followedBy: ExtendedEntry<OntimeEvent> | null;
|
||||
}
|
||||
|
||||
export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) {
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const { clock, offset } = useTimelineSocket();
|
||||
const state = useExpectedStartData();
|
||||
|
||||
// gather card data
|
||||
const titleNow = now?.title ?? '-';
|
||||
@@ -26,20 +27,20 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect
|
||||
let followedByStatus: string | undefined;
|
||||
|
||||
if (next !== null) {
|
||||
const timeToStart = getTimeToStart(clock, next.timeStart, next?.delay ?? 0, offset);
|
||||
if (timeToStart < 0) {
|
||||
const { timeToStart } = getExpectedTimesFromExtendedEvent(next, state);
|
||||
if (timeToStart <= 0) {
|
||||
nextStatus = dueText;
|
||||
} else {
|
||||
nextStatus = `T - ${formatDuration(timeToStart)}`;
|
||||
nextStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (followedBy !== null) {
|
||||
const timeToStart = getTimeToStart(clock, followedBy.timeStart, followedBy?.delay ?? 0, offset);
|
||||
if (timeToStart < 0) {
|
||||
const { timeToStart } = getExpectedTimesFromExtendedEvent(followedBy, state);
|
||||
if (timeToStart <= 0) {
|
||||
followedByStatus = dueText;
|
||||
} else {
|
||||
followedByStatus = `T - ${formatDuration(timeToStart)}`;
|
||||
followedByStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect
|
||||
title={getLocalizedString('timeline.followedby')}
|
||||
status={followedByStatus}
|
||||
content={followedByText}
|
||||
category='next'
|
||||
category='followedBy'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MaybeString } from 'ontime-types';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
interface SectionProps {
|
||||
category: 'now' | 'next';
|
||||
category: 'now' | 'next' | 'followedBy';
|
||||
content: MaybeString;
|
||||
title: string;
|
||||
status?: string;
|
||||
@@ -16,7 +16,7 @@ function TimelineSection({ category, content, title, status }: SectionProps) {
|
||||
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
||||
const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']);
|
||||
return (
|
||||
<div className={sectionClasses}>
|
||||
<div className={sectionClasses} data-testid={category}>
|
||||
<div className='section-title'>
|
||||
<span className='section-title__label'>{title}</span>
|
||||
{status && <span className='section-title__status'>{status}</span>}
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
getTimeFrom,
|
||||
isNewLatest,
|
||||
MILLIS_PER_HOUR,
|
||||
MILLIS_PER_MINUTE,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { formatDuration } from '../../common/utils/time';
|
||||
|
||||
import { useTimelineOptions } from './timeline.options';
|
||||
@@ -76,20 +78,23 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str
|
||||
return status;
|
||||
}
|
||||
|
||||
if (timeToStart < 0) {
|
||||
if (timeToStart <= 0) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
return formatDuration(timeToStart);
|
||||
return formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2);
|
||||
}
|
||||
|
||||
interface ScopedRundownData {
|
||||
scopedRundown: PlayableEvent[];
|
||||
scopedRundown: ExtendedEntry<PlayableEvent>[];
|
||||
firstStart: number;
|
||||
totalDuration: number;
|
||||
}
|
||||
|
||||
export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeString): ScopedRundownData {
|
||||
export function useScopedRundown(
|
||||
rundown: ExtendedEntry<OntimeEntry>[],
|
||||
selectedEventId: MaybeString,
|
||||
): ScopedRundownData {
|
||||
const { hidePast } = useTimelineOptions();
|
||||
|
||||
const data = useMemo(() => {
|
||||
@@ -97,11 +102,11 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
|
||||
return { scopedRundown: [], firstStart: 0, totalDuration: 0 };
|
||||
}
|
||||
|
||||
const scopedRundown: PlayableEvent[] = [];
|
||||
const scopedRundown: ExtendedEntry<PlayableEvent>[] = [];
|
||||
let selectedIndex = selectedEventId ? Infinity : -1;
|
||||
let firstStart = null;
|
||||
let totalDuration = 0;
|
||||
let lastEntry: PlayableEvent | null = null;
|
||||
let lastEntry: ExtendedEntry<PlayableEvent> | null = null;
|
||||
|
||||
for (let i = 0; i < rundown.length; i++) {
|
||||
const currentEntry = rundown[i];
|
||||
@@ -150,26 +155,28 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
|
||||
}
|
||||
|
||||
type UpcomingEvents = {
|
||||
now: OntimeEvent | null;
|
||||
next: OntimeEvent | null;
|
||||
followedBy: OntimeEvent | null;
|
||||
now: ExtendedEntry<OntimeEvent> | null;
|
||||
next: ExtendedEntry<OntimeEvent> | null;
|
||||
followedBy: ExtendedEntry<OntimeEvent> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns upcoming events from current: now, next and followedBy
|
||||
*/
|
||||
export function getUpcomingEvents(events: PlayableEvent[], selectedId: MaybeString): UpcomingEvents {
|
||||
export function getUpcomingEvents(events: ExtendedEntry<PlayableEvent>[], selectedId: MaybeString): UpcomingEvents {
|
||||
if (events.length === 0) {
|
||||
return { now: null, next: null, followedBy: null };
|
||||
}
|
||||
|
||||
let now = selectedId ? getEventWithId(events, selectedId) : null;
|
||||
let now = selectedId ? (getEventWithId(events, selectedId) as ExtendedEntry<OntimeEvent>) : null;
|
||||
if (!isOntimeEvent(now)) {
|
||||
now = null;
|
||||
}
|
||||
|
||||
const next = now ? getNextEvent(events, now.id)?.nextEvent : getFirstEvent(events).firstEvent;
|
||||
const followedBy = next ? getNextEvent(events, next.id)?.nextEvent : null;
|
||||
const next = now
|
||||
? (getNextEvent(events, now.id)?.nextEvent as ExtendedEntry<OntimeEvent> | null)
|
||||
: (getFirstEvent(events).firstEvent as ExtendedEntry<OntimeEvent> | null);
|
||||
const followedBy = next ? (getNextEvent(events, next.id)?.nextEvent as ExtendedEntry<OntimeEvent> | null) : null;
|
||||
|
||||
// Return the titles, handling nulls appropriately
|
||||
return {
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { OntimeEntry, ProjectData, Settings } from 'ontime-types';
|
||||
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||
import { useFlatRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||
import useSettings from '../../common/hooks-query/useSettings';
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
||||
|
||||
export interface TimelineData {
|
||||
events: OntimeEntry[];
|
||||
events: ExtendedEntry<OntimeEntry>[];
|
||||
projectData: ProjectData;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export function useTimelineData(): ViewData<TimelineData> {
|
||||
// HTTP API data
|
||||
const { data: rundownData, status: rundownStatus } = useFlatRundown();
|
||||
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||
const { data: settings, status: settingsStatus } = useSettings();
|
||||
|
||||
|
||||
@@ -8,14 +8,17 @@ import svgrPlugin from 'vite-plugin-svgr';
|
||||
import { ONTIME_VERSION } from './src/ONTIME_VERSION';
|
||||
|
||||
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
|
||||
const isDev = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'development';
|
||||
|
||||
export default defineConfig({
|
||||
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
|
||||
define: {
|
||||
// we pass along the NODE_ENV here in case it is a docker build
|
||||
'import.meta.env.IS_DOCKER': process.env.NODE_ENV === 'docker',
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
svgrPlugin(),
|
||||
!isDev &&
|
||||
sentryAuthToken &&
|
||||
sentryVitePlugin({
|
||||
org: 'get-ontime',
|
||||
project: 'ontime',
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "ontime-electron",
|
||||
"version": "4.0.0-beta.4",
|
||||
"version": "4.0.0-beta.5",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
"keywords": [
|
||||
"lighdev",
|
||||
"ontime",
|
||||
"timer"
|
||||
"timer",
|
||||
"stage timer",
|
||||
"rundown"
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "src/main.js",
|
||||
"devDependencies": {
|
||||
"electron": "37.2.1",
|
||||
"electron": "38.2.1",
|
||||
"electron-builder": "26.0.18",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
@@ -20,14 +21,12 @@
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "",
|
||||
"dev:electron": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .",
|
||||
"lint": "eslint . --quiet",
|
||||
"dev": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .",
|
||||
"dist-win": "electron-builder --publish=never --x64 --win",
|
||||
"dist-mac": "electron-builder --publish=never --mac",
|
||||
"dist-mac:local": "electron-builder --publish=never --mac -c.mac.identity=null",
|
||||
"dist-linux": "electron-builder --publish=never --x64 --linux",
|
||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
"dist-linux": "electron-builder --publish=never --x64 --linux"
|
||||
},
|
||||
"build": {
|
||||
"productName": "ontime-prerelease",
|
||||
|
||||
@@ -218,7 +218,7 @@ function makeSettingsMenu(redirectWindow) {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Project data',
|
||||
label: 'Project settings',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Rundown defaults',
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/resolver",
|
||||
"version": "4.0.0-beta.4",
|
||||
"version": "4.0.0-beta.5",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
"types": "./dist/main.d.ts",
|
||||
@@ -8,6 +8,7 @@
|
||||
"description": "shared typings for ontime",
|
||||
"scripts": {
|
||||
"lint": "eslint . --quiet",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prebuild": "pnpm rimraf ./dist",
|
||||
"build": "tsup && pnpm rimraf ./dist/index.js",
|
||||
"postbuild": "pnpm rimraf ./dist/index.d.ts"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import { esbuildCommon } from './esbuildCommon.js';
|
||||
|
||||
await esbuild.build({
|
||||
...esbuildCommon,
|
||||
entryPoints: ['src/app.ts'],
|
||||
minify: false,
|
||||
dropLabels: [],
|
||||
outfile: 'dist/index.cjs',
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import { esbuildCommon } from './esbuildCommon.js';
|
||||
|
||||
await esbuild.build({
|
||||
...esbuildCommon,
|
||||
entryPoints: ['src/index.ts'],
|
||||
outfile: 'dist/docker.cjs',
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import { esbuildCommon } from './esbuildCommon.js';
|
||||
|
||||
await esbuild.build({
|
||||
...esbuildCommon,
|
||||
entryPoints: ['src/app.ts'],
|
||||
outfile: 'dist/index.cjs',
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
|
||||
const isDocker = process.env.NODE_ENV === 'docker';
|
||||
|
||||
await esbuild.build({
|
||||
logLevel: 'error',
|
||||
platform: 'node',
|
||||
target: ['node20'],
|
||||
format: 'cjs',
|
||||
bundle: true,
|
||||
minify: true,
|
||||
legalComments: 'external',
|
||||
dropLabels: ['DEV'],
|
||||
entryPoints: isDocker ? ['src/index.ts'] : ['src/app.ts'],
|
||||
outfile: isDocker ? 'dist/docker.cjs' : 'dist/index.cjs',
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
export const esbuildCommon = {
|
||||
logLevel: 'error',
|
||||
platform: 'node',
|
||||
target: ['node20'],
|
||||
format: 'cjs',
|
||||
bundle: true,
|
||||
minify: true,
|
||||
legalComments: 'external',
|
||||
dropLabels: ['DEV'],
|
||||
};
|
||||
+10
-11
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "4.0.0-beta.4",
|
||||
"version": "4.0.0-beta.5",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
@@ -48,19 +48,18 @@
|
||||
"scripts": {
|
||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||
"postinstall": "pnpm addversion",
|
||||
|
||||
"dev": "cross-env NODE_ENV=development tsx watch ./src/index.ts",
|
||||
"dev:electron": "pnpm dev",
|
||||
"dev:inspect": "cross-env NODE_ENV=development tsx watch --inspect ./src/index.ts",
|
||||
"dev:test": "cross-env IS_TEST=true tsx ./src/index.ts",
|
||||
"prebuild": "tsx ./scripts/bundleCss.ts && tsx ./scripts/bundleTranslation.ts",
|
||||
"build": "node esbuild.electron.js",
|
||||
"build:electron": "node esbuild.electron.js",
|
||||
"build:local": "node esbuild.dev.js",
|
||||
"build:docker": "node esbuild.docker.js",
|
||||
"build:localdocker": "cross-env NODE_ENV=local node esbuild.docker.js",
|
||||
|
||||
"lint": "eslint . --quiet",
|
||||
"test": "cross-env IS_TEST=true vitest",
|
||||
"test:pipeline": "cross-env IS_TEST=true vitest run",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
|
||||
"prebuild": "tsx ./scripts/bundleCss.ts && tsx ./scripts/bundleTranslation.ts",
|
||||
"build": "node esbuild.js",
|
||||
|
||||
"test": "cross-env IS_TEST=true vitest",
|
||||
"test:pipeline": "cross-env IS_TEST=true vitest run"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { CustomFields, OntimeGroup, OntimeDelay, OntimeEvent, SupportedEntry, TimeStrategy, OntimeMilestone } from 'ontime-types';
|
||||
import {
|
||||
CustomFields,
|
||||
OntimeGroup,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
SupportedEntry,
|
||||
TimeStrategy,
|
||||
OntimeMilestone,
|
||||
} from 'ontime-types';
|
||||
import { dayInMs, MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import {
|
||||
@@ -1641,6 +1649,38 @@ describe('rundownMutation.group()', () => {
|
||||
'3': { id: '3', type: SupportedEntry.Event, parent: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('BUG: creates a group in between groups', () => {
|
||||
const rundown = makeRundown({
|
||||
order: ['group-1', '1', '2', '3', '4', 'group-2'],
|
||||
entries: {
|
||||
'group-1': makeOntimeGroup({ id: 'group-1', entries: ['1a', '1b'] }),
|
||||
'group-2': makeOntimeGroup({ id: 'group-2', entries: ['2a', '2b'] }),
|
||||
'1': makeOntimeEvent({ id: '1', parent: null }),
|
||||
'2': makeOntimeEvent({ id: '2', parent: null }),
|
||||
'3': makeOntimeEvent({ id: '3', parent: null }),
|
||||
'4': makeOntimeEvent({ id: '4', parent: null }),
|
||||
'1a': makeOntimeEvent({ id: '1a', parent: 'group-1' }),
|
||||
'1b': makeOntimeEvent({ id: '1b', parent: 'group-1' }),
|
||||
'2a': makeOntimeEvent({ id: '2a', parent: 'group-2' }),
|
||||
'2b': makeOntimeEvent({ id: '2b', parent: 'group-2' }),
|
||||
},
|
||||
});
|
||||
|
||||
rundownMutation.group(rundown, ['2', '3']);
|
||||
|
||||
const groupId = rundown.order[2];
|
||||
expect(groupId).toStrictEqual(expect.any(String));
|
||||
expect(rundown.order).toStrictEqual(['group-1', '1', expect.any(String), '4', 'group-2']);
|
||||
expect(rundown.entries).toMatchObject({
|
||||
[groupId]: {
|
||||
type: SupportedEntry.Group,
|
||||
entries: ['2', '3'],
|
||||
},
|
||||
'2': { id: '2', type: SupportedEntry.Event, parent: groupId },
|
||||
'3': { id: '3', type: SupportedEntry.Event, parent: groupId },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rundownMutation.ungroup()', () => {
|
||||
|
||||
@@ -498,7 +498,7 @@ function group(rundown: Rundown, entryIds: EntryId[]): OntimeGroup {
|
||||
// the group will be created at the first selected event position
|
||||
// note that this is not the lowest index
|
||||
if (firstIndex === -1) {
|
||||
firstIndex = rundown.flatOrder.indexOf(entryId);
|
||||
firstIndex = rundown.order.indexOf(entryId);
|
||||
}
|
||||
|
||||
nestedEvents.push(entryId);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { dayInMs, MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { loadRoll } from '../rollUtils.js';
|
||||
import { makeRundown } from '../../api-data/rundown/__mocks__/rundown.mocks.js';
|
||||
import { PlayableEvent } from 'ontime-types';
|
||||
|
||||
import { initRundown } from '../../api-data/rundown/rundown.service.js';
|
||||
import { rundownCache } from '../../api-data/rundown/rundown.dao.js';
|
||||
import { processRundown, rundownCache } from '../../api-data/rundown/rundown.dao.js';
|
||||
import { makeOntimeEvent, makeRundown } from '../../api-data/rundown/__mocks__/rundown.mocks.js';
|
||||
|
||||
import { loadRoll } from '../rollUtils.js';
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock('../../classes/data-provider/DataProvider.js', () => {
|
||||
@@ -198,6 +200,24 @@ describe('loadRoll()', () => {
|
||||
const state = loadRoll(rundown, metadata, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should ignore events with 0 duration', () => {
|
||||
const rundown = makeRundown({
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 70, timeEnd: 70, duration: 0 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 70, timeEnd: 170, duration: 100 }),
|
||||
},
|
||||
order: ['1', '2'],
|
||||
});
|
||||
const metadata = processRundown(rundown, {});
|
||||
const now = 70;
|
||||
|
||||
const state = loadRoll(rundown, metadata, now);
|
||||
expect(state).toStrictEqual({
|
||||
event: rundown.entries['2'],
|
||||
index: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRoll() handle edge cases with midnight', () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ export function loadRoll(
|
||||
|
||||
for (let i = 0; i < metadata.playableEventOrder.length; i++) {
|
||||
const event = rundown.entries[metadata.playableEventOrder[i]] as PlayableEvent;
|
||||
// rolling into events of 0 duration would make the playback be stuck
|
||||
if (event.duration === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
EndAction,
|
||||
EntryId,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
LogOrigin,
|
||||
Offset,
|
||||
OffsetMode,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
@@ -87,18 +89,17 @@ class RuntimeService {
|
||||
// 2. handle edge cases related to roll
|
||||
if (newState.timer.playback === Playback.Roll) {
|
||||
// check if we need to call any side effects
|
||||
const keepOffset = newState.offset.absolute;
|
||||
if (hasSecondaryTimerFinished) {
|
||||
// if the secondary timer has finished, we need to call roll
|
||||
// since event is already loaded
|
||||
this.rollLoaded(keepOffset);
|
||||
this.rollLoaded(newState.offset);
|
||||
} else if (hasTimerFinished) {
|
||||
// if the timer has finished, we need to load next and keep rolling
|
||||
process.nextTick(() => {
|
||||
triggerAutomations(TimerLifeCycle.onFinish, newState);
|
||||
});
|
||||
this.handleLoadNext();
|
||||
this.rollLoaded(keepOffset);
|
||||
this.rollLoaded(newState.offset);
|
||||
} else if (
|
||||
// if there is no previous clock, we could not have skipped
|
||||
RuntimeService.previousState?.clock &&
|
||||
@@ -386,11 +387,12 @@ class RuntimeService {
|
||||
* startSelected being a private function does not trigger emits
|
||||
* and pass on runtime offset in case of roll mode
|
||||
*/
|
||||
private handleLoadNext(): boolean {
|
||||
private handleLoadNext(fromId?: EntryId): boolean {
|
||||
const state = runtimeState.getState();
|
||||
const { playableEventOrder } = getRundownMetadata();
|
||||
|
||||
const nextId = findNextPlayableId(playableEventOrder, state.eventNow?.id);
|
||||
const nextId = findNextPlayableId(playableEventOrder, fromId ?? state.eventNow?.id);
|
||||
|
||||
if (nextId) {
|
||||
const nextEvent = getEntryWithId(nextId);
|
||||
if (!nextEvent || !isOntimeEvent(nextEvent)) {
|
||||
@@ -398,6 +400,14 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
if (state.timer.playback === Playback.Roll) {
|
||||
if (nextEvent.duration === 0) {
|
||||
/**
|
||||
* when loading next in roll mode,
|
||||
* we need to prevent loading events of 0 duration since
|
||||
* this would make the playback be stuck
|
||||
*/
|
||||
return this.handleLoadNext(nextId);
|
||||
}
|
||||
return this.loadEvent(nextEvent, { firstStart: state.rundown.actualStart });
|
||||
}
|
||||
return this.loadEvent(nextEvent);
|
||||
@@ -531,7 +541,7 @@ class RuntimeService {
|
||||
/**
|
||||
* Handles special case to call roll on a loaded event which we do not want to discard
|
||||
*/
|
||||
private rollLoaded(offset?: number) {
|
||||
private rollLoaded(offset: Offset) {
|
||||
const rundown = getCurrentRundown();
|
||||
const metadata = getRundownMetadata();
|
||||
|
||||
|
||||
@@ -357,18 +357,18 @@ describe('roll mode', () => {
|
||||
start();
|
||||
// the current offset after manual play
|
||||
const currentOffset = getState().offset.absolute;
|
||||
let result = roll(rundown, metadata, getState().offset.absolute);
|
||||
let result = roll(rundown, metadata, getState().offset);
|
||||
expect(result).toStrictEqual({ eventId: '1', didStart: false });
|
||||
// the current offset should be maintain by roll mode when taking over from play
|
||||
expect(getState().offset.absolute).toBe(currentOffset);
|
||||
|
||||
vi.setSystemTime('jan 1 00:00:01');
|
||||
result = roll(rundown, metadata, getState().offset.absolute);
|
||||
result = roll(rundown, metadata, getState().offset);
|
||||
expect(result).toStrictEqual({ eventId: '2', didStart: true });
|
||||
expect(getState().offset.absolute).toBe(-1000);
|
||||
|
||||
vi.setSystemTime('jan 1 00:00:02');
|
||||
result = roll(rundown, metadata, getState().offset.absolute);
|
||||
result = roll(rundown, metadata, getState().offset);
|
||||
expect(result).toStrictEqual({ eventId: '3', didStart: true });
|
||||
expect(getState().offset.absolute).toBe(-1000);
|
||||
|
||||
|
||||
@@ -595,7 +595,7 @@ export function update(): UpdateResult {
|
||||
export function roll(
|
||||
rundown: Rundown,
|
||||
metadata: RundownMetadata,
|
||||
offset = 0,
|
||||
offset?: Offset,
|
||||
): { eventId: MaybeString; didStart: boolean } {
|
||||
// 1. if an event is running, we simply take over the playback
|
||||
if (runtimeState.timer.playback === Playback.Play && runtimeState.rundown.selectedEventIndex !== null) {
|
||||
@@ -616,7 +616,9 @@ export function roll(
|
||||
}
|
||||
}
|
||||
|
||||
runtimeState.offset.absolute = offset;
|
||||
if (offset) {
|
||||
runtimeState.offset = { ...offset };
|
||||
}
|
||||
runtimeState.timer.playback = Playback.Roll;
|
||||
|
||||
// account for event that finishes the day after
|
||||
@@ -626,7 +628,7 @@ export function roll(
|
||||
: runtimeState.eventNow.timeEnd;
|
||||
runtimeState.timer.expectedFinish = normalisedEndTime;
|
||||
|
||||
//account for offset
|
||||
// account for offset
|
||||
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||
|
||||
// state catch up
|
||||
@@ -672,8 +674,10 @@ export function roll(
|
||||
// we need to persist the current group state across loads
|
||||
clearEventData();
|
||||
|
||||
//account for offset but we only keep it if passed to us
|
||||
runtimeState.offset.absolute = offset;
|
||||
// account for offset but we only keep it if passed to us
|
||||
if (offset) {
|
||||
runtimeState.offset = { ...offset };
|
||||
}
|
||||
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||
|
||||
const { index, isPending } = loadRoll(rundown, metadata, offsetClock);
|
||||
|
||||
@@ -2,11 +2,16 @@ import { test, expect } from '@playwright/test';
|
||||
|
||||
test('smoke test operator', async ({ page }) => {
|
||||
// make some boilerplate
|
||||
await page.goto('http://localhost:4001/editor');
|
||||
await page.goto('/editor');
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
await page.getByRole('button', { name: 'Clear all' }).click();
|
||||
await page.getByRole('button', { name: 'Delete all' }).click();
|
||||
await page.getByRole('button', { name: 'Create Event' }).click();
|
||||
await page.getByRole('button', { name: 'Create Group' }).click();
|
||||
await page.getByTestId('rundown-group').getByTestId('entry__title').click();
|
||||
await page.getByTestId('rundown-group').getByTestId('entry__title').fill('group 1');
|
||||
await page.getByTestId('rundown-group').getByTestId('entry__title').press('Enter');
|
||||
|
||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
|
||||
await page.getByTestId('time-input-timeStart').fill('1m');
|
||||
await page.getByTestId('time-input-timeStart').press('Enter');
|
||||
@@ -24,10 +29,9 @@ test('smoke test operator', async ({ page }) => {
|
||||
await page.getByTestId('entry-3').getByTestId('time-input-duration').fill('1m');
|
||||
await page.getByTestId('entry-3').getByTestId('time-input-duration').press('Enter');
|
||||
|
||||
await page.getByRole('button', { name: 'Group' }).nth(1).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
await page.getByTestId('entry-1').click();
|
||||
await page.getByTestId('editor-container').getByLabel('Cue').fill('--1');
|
||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
|
||||
await page.getByTestId('entry-1').getByTestId('entry__title').click();
|
||||
@@ -51,9 +55,11 @@ test('smoke test operator', async ({ page }) => {
|
||||
// start an event
|
||||
await page.getByTestId('panel-timer-control').getByRole('button', { name: 'Start' }).click();
|
||||
|
||||
await page.goto('http://localhost:4001/op');
|
||||
await page.goto('/op');
|
||||
|
||||
await expect(page.getByText('group 1')).toBeInViewport();
|
||||
await expect(page.getByText('title 1')).toBeInViewport();
|
||||
await expect(page.getByTestId('--1')).toHaveCSS('opacity', '1'); // BUG: ensure event doesn't inherit the past state of the group
|
||||
await expect(page.getByText('title 2')).toBeInViewport();
|
||||
await expect(page.getByText('title 3')).toBeInViewport();
|
||||
|
||||
|
||||
@@ -92,12 +92,13 @@ test.describe('URL Preset', () => {
|
||||
// select options
|
||||
await page.getByRole('combobox').filter({ hasText: 'Timer' }).click();
|
||||
await page.getByText('URL Preset: testing').click();
|
||||
await page.locator('button[name="lockNav"]').click();
|
||||
await page.locator('button[name="lockConfig"]').click();
|
||||
await page.getByTestId('lockNav').click();
|
||||
await page.getByTestId('lockConfig').click();
|
||||
|
||||
// create and verify link
|
||||
await page.getByRole('button', { name: 'Create share link' }).click();
|
||||
await expect(page.getByTestId('copy-link')).toContainText('/preset/testing');
|
||||
await expect(page.getByTestId('copy-link')).toContainText('/preset/testing');
|
||||
await expect(page.getByTestId('copy-link')).toContainText('n=1');
|
||||
|
||||
// verify the preset
|
||||
@@ -140,7 +141,7 @@ test.describe('Sharing from cuesheet', () => {
|
||||
await page.getByRole('textbox').fill('cuesheet-read-test');
|
||||
await page.getByText('Custom write').click();
|
||||
await page.getByText('Custom read').click();
|
||||
await page.locator('button[name="lockNav"]').click();
|
||||
await page.getByTestId('lockNav').click();
|
||||
await page.getByTestId('write-flag').click();
|
||||
await page.getByTestId('write-cue').click();
|
||||
await page.getByTestId('write-title').click();
|
||||
@@ -184,7 +185,7 @@ test.describe('Sharing from cuesheet', () => {
|
||||
await page.getByRole('textbox').fill('cuesheet-scope-test');
|
||||
await page.getByText('Custom write').click();
|
||||
await page.getByText('Custom read').click();
|
||||
await page.locator('button[name="lockNav"]').click();
|
||||
await page.getByTestId('lockNav').click();
|
||||
await page.getByTestId('write-flag').click();
|
||||
await page.getByTestId('write-cue').click();
|
||||
await page.getByTestId('write-timeStart').click();
|
||||
|
||||
@@ -1,77 +1,125 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, Locator, Page, test } from '@playwright/test';
|
||||
|
||||
test('time until absolute', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/editor');
|
||||
test('time until absolute', async ({ context }) => {
|
||||
const editor = await context.newPage();
|
||||
const op = await context.newPage();
|
||||
const timeline = await context.newPage();
|
||||
const countdown = await context.newPage();
|
||||
await editor.goto('/editor');
|
||||
await op.goto('/op');
|
||||
await timeline.goto('/timeline');
|
||||
|
||||
await page.getByRole('button', { name: 'Clear all' }).click();
|
||||
await page.getByRole('button', { name: 'Delete all' }).click();
|
||||
await editor.getByRole('button', { name: 'Clear all' }).click();
|
||||
await editor.getByRole('button', { name: 'Delete all' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Create Event' }).click();
|
||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await editor.getByRole('button', { name: 'Create Event' }).click();
|
||||
await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Absolute' }).click();
|
||||
await page.getByTestId('entry-1').getByLabel('Start event').click();
|
||||
await expect(page.getByTestId('offset')).not.toContainText('00:00:00'); // This might be a bad test requires that the test is not run at 0h
|
||||
await page.getByLabel('Pause event').click();
|
||||
await editor.getByTestId('entry-1').getByTestId('rundown-event').click();
|
||||
const ids = new Array<string>();
|
||||
ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue());
|
||||
await editor.getByTestId('entry-2').getByTestId('rundown-event').click();
|
||||
ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue());
|
||||
await editor.getByTestId('entry-3').getByTestId('rundown-event').click();
|
||||
ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue());
|
||||
await editor.getByTestId('entry-4').getByTestId('rundown-event').click();
|
||||
ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue());
|
||||
|
||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m');
|
||||
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m');
|
||||
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m');
|
||||
await countdown.goto('/countdown?' + ids.join('&sub='));
|
||||
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
const locatorElements2 = [
|
||||
editor.getByTestId('entry-2').getByTestId('rundown-event'),
|
||||
op.getByTestId('2').getByTestId('time-until'),
|
||||
timeline.getByTestId('2'),
|
||||
timeline.getByTestId('next'),
|
||||
countdown.getByTestId('2'),
|
||||
];
|
||||
const locatorElements3 = [
|
||||
editor.getByTestId('entry-3').getByTestId('rundown-event'),
|
||||
op.getByTestId('3').getByTestId('time-until'),
|
||||
timeline.getByTestId('3'),
|
||||
timeline.getByTestId('followedBy'),
|
||||
countdown.getByTestId('3'),
|
||||
];
|
||||
const locatorElements4 = [
|
||||
editor.getByTestId('entry-4').getByTestId('rundown-event'),
|
||||
op.getByTestId('4').getByTestId('time-until'),
|
||||
timeline.getByTestId('4'),
|
||||
countdown.getByTestId('4'),
|
||||
];
|
||||
|
||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m');
|
||||
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m');
|
||||
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m');
|
||||
await editor.getByRole('button', { name: 'Absolute' }).click();
|
||||
await editor.getByTestId('entry-1').getByLabel('Start event').click();
|
||||
await expect(editor.getByTestId('offset')).not.toContainText('00:00:00'); // This might be a bad test requires that the test is not run at 0h
|
||||
await editor.getByLabel('Pause event').click();
|
||||
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
await testTimes(locatorElements2, '9m');
|
||||
await testTimes(locatorElements3, '19m');
|
||||
await testTimes(locatorElements4, '29m');
|
||||
|
||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
|
||||
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
|
||||
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
|
||||
await testTimes(locatorElements2, '5h59m');
|
||||
await testTimes(locatorElements3, '6h9m');
|
||||
await testTimes(locatorElements4, '6h19m');
|
||||
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
|
||||
await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
|
||||
await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
|
||||
await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
|
||||
|
||||
await testTimes(locatorElements2, '30s');
|
||||
await testTimes(locatorElements3, '10m');
|
||||
await testTimes(locatorElements4, '20m');
|
||||
});
|
||||
|
||||
test('time until relative', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/editor');
|
||||
async function testTimes(locators: Locator[], value: string) {
|
||||
await locators.map(async (locator) => await expect(locator).toContainText(value));
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Clear all' }).click();
|
||||
await page.getByRole('button', { name: 'Delete all' }).click();
|
||||
test('time until relative', async ({ context }) => {
|
||||
const editor = await context.newPage();
|
||||
editor.goto('http://localhost:4001/editor');
|
||||
|
||||
await page.getByRole('button', { name: 'Create Event' }).click();
|
||||
await page.getByRole('button', { name: 'Event' }).nth(4).click();
|
||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await editor.getByRole('button', { name: 'Clear all' }).click();
|
||||
await editor.getByRole('button', { name: 'Delete all' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Relative' }).click();
|
||||
await page.getByTestId('entry-1').getByLabel('Start event').click();
|
||||
await expect(page.getByTestId('offset')).toContainText('00:00:00'); // This might be a bad test as it ruires the evaluation to happen within 1s
|
||||
await page.getByLabel('Pause event').click();
|
||||
await editor.getByRole('button', { name: 'Create Event' }).click();
|
||||
await editor.getByRole('button', { name: 'Event' }).nth(4).click();
|
||||
await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||
|
||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m');
|
||||
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m');
|
||||
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m');
|
||||
await editor.getByRole('button', { name: 'Relative' }).click();
|
||||
await editor.getByTestId('entry-1').getByLabel('Start event').click();
|
||||
await expect(editor.getByTestId('offset')).toContainText('00:00:00'); // This might be a bad test as it ruires the evaluation to happen within 1s
|
||||
await editor.getByLabel('Pause event').click();
|
||||
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m');
|
||||
await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m');
|
||||
await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m');
|
||||
|
||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m');
|
||||
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m');
|
||||
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m');
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
|
||||
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m');
|
||||
await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m');
|
||||
await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m');
|
||||
|
||||
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
|
||||
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
|
||||
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').click();
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
|
||||
await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
|
||||
|
||||
await page.getByRole('button', { name: 'Absolute' }).click();
|
||||
await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
|
||||
await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
|
||||
await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
|
||||
|
||||
await editor.getByRole('button', { name: 'Absolute' }).click();
|
||||
});
|
||||
|
||||
+12
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "4.0.0-beta.4",
|
||||
"version": "4.0.0-beta.5",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"ontime",
|
||||
@@ -22,19 +22,20 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"dev:electron": "turbo run dev:electron",
|
||||
"lint": "turbo run lint",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"build": "turbo run build",
|
||||
"build:local": "turbo run build:local",
|
||||
"build:electron": "turbo run build:electron",
|
||||
"build:localdocker": "turbo run build:localdocker",
|
||||
"build:docker": "cross-env NODE_ENV=docker turbo run build --filter=ontime-server --filter=ontime-ui",
|
||||
"build:resolver": "cross-env turbo run build --filter=@getontime/resolver",
|
||||
"dist-win": "turbo run dist-win",
|
||||
"dist-mac": "turbo run dist-mac",
|
||||
"dist-linux": "turbo run dist-linux",
|
||||
"e2e": "pnpm clear-temp && cross-env DEBUG=pw:webserver npx playwright test -c playwright.config.ts",
|
||||
"e2e:ui": "cross-env DEBUG=pw:webserver npx playwright test --ui -c playwright.config.ts",
|
||||
"e2e": "turbo run build --filter=ontime-ui && pnpm clear-temp && cross-env DEBUG=pw:webserver npx playwright test -c playwright.config.ts",
|
||||
"e2e:ui": "cross-env NODE_ENV=development cross-env DEBUG=pw:webserver npx playwright test --ui -c playwright.config.ts",
|
||||
"e2e:i": "npx playwright codegen",
|
||||
"cleanup": "pnpm rimraf node_modules && rimraf **/node_modules && rimraf **/**/node_modules",
|
||||
"clear-temp": "rm -rf e2e/tests/fixtures/tmp"
|
||||
"clear-temp": "rimraf e2e/tests/fixtures/tmp",
|
||||
"test": "turbo run test:pipeline"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.1",
|
||||
@@ -46,9 +47,9 @@
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-plugin-playwright": "^1.5.2",
|
||||
"prettier": "catalog:",
|
||||
"turbo": "^2.3.3",
|
||||
"turbo": "2.5.8",
|
||||
"typescript": "catalog:",
|
||||
"rimraf": "catalog:"
|
||||
"rimraf": "catalog:"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||
"packageManager": "pnpm@10.18.0+sha512.e804f889f1cecc40d572db084eec3e4881739f8dec69c0ff10d2d1beff9a4e309383ba27b5b750059d7f4c149535b6cd0d2cb1ed3aeb739239a4284a68f40cfa"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -7,8 +7,8 @@
|
||||
"private": true,
|
||||
"description": "shared typings for ontime",
|
||||
"scripts": {
|
||||
"cleanup": "rm -rf .turbo && rm -rf node_modules",
|
||||
"lint": "eslint . --quiet",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -7,8 +7,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint . --quiet",
|
||||
"test": "vitest",
|
||||
"test:pipeline": "vitest run",
|
||||
"cleanup": "rm -rf .turbo && rm -rf node_modules"
|
||||
"test:pipeline": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge-ts": "^7.0.3",
|
||||
|
||||
+16
-7
@@ -1,6 +1,8 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
import { devices } from '@playwright/test';
|
||||
|
||||
const isDevMode = process.env.NODE_ENV === 'development';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
@@ -15,18 +17,25 @@ const config: PlaywrightTestConfig = {
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
webServer: {
|
||||
command: 'turbo run dev --filter=ontime-server',
|
||||
port: 4001,
|
||||
reuseExistingServer: true,
|
||||
timeout: 60 * 1000,
|
||||
},
|
||||
webServer: isDevMode
|
||||
? {
|
||||
command: 'turbo run dev',
|
||||
port: 3000,
|
||||
reuseExistingServer: true,
|
||||
timeout: 60 * 1000,
|
||||
}
|
||||
: {
|
||||
command: 'turbo run dev --filter=ontime-server',
|
||||
port: 4001,
|
||||
reuseExistingServer: true,
|
||||
timeout: 60 * 1000,
|
||||
},
|
||||
use: {
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
actionTimeout: 5000,
|
||||
baseURL: 'http://localhost:4001',
|
||||
baseURL: isDevMode ? 'http://localhost:3000' : 'http://localhost:4001',
|
||||
ignoreHTTPSErrors: true,
|
||||
trace: 'on-first-retry',
|
||||
launchOptions: {
|
||||
|
||||
Generated
+54
-76
@@ -75,8 +75,8 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.1
|
||||
turbo:
|
||||
specifier: ^2.3.3
|
||||
version: 2.5.6
|
||||
specifier: 2.5.8
|
||||
version: 2.5.8
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.3
|
||||
@@ -96,8 +96,8 @@ importers:
|
||||
apps/client:
|
||||
dependencies:
|
||||
'@base-ui-components/react':
|
||||
specifier: 1.0.0-beta.3
|
||||
version: 1.0.0-beta.3(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
specifier: 1.0.0-beta.4
|
||||
version: 1.0.0-beta.4(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
@@ -132,8 +132,8 @@ importers:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
axios:
|
||||
specifier: ^1.11.0
|
||||
version: 1.11.0
|
||||
specifier: ^1.12.2
|
||||
version: 1.12.2
|
||||
babel-plugin-react-compiler:
|
||||
specifier: 19.1.0-rc.3
|
||||
version: 19.1.0-rc.3
|
||||
@@ -262,8 +262,8 @@ importers:
|
||||
apps/electron:
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: 37.2.1
|
||||
version: 37.2.1
|
||||
specifier: 38.2.1
|
||||
version: 38.2.1
|
||||
electron-builder:
|
||||
specifier: 26.0.18
|
||||
version: 26.0.18(electron-builder-squirrel-windows@24.13.3)
|
||||
@@ -699,8 +699,8 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.3':
|
||||
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.22.15':
|
||||
@@ -743,8 +743,8 @@ packages:
|
||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@base-ui-components/react@1.0.0-beta.3':
|
||||
resolution: {integrity: sha512-4sAq6zmDA9ixV2HRjjeM1+tSEw5R6nvGjXUQmFoQnC3DZLEUdwO94gWDmUDdpoDuChn27jdbaJs9F0Ih4w2UAA==}
|
||||
'@base-ui-components/react@1.0.0-beta.4':
|
||||
resolution: {integrity: sha512-sPYKj26gbFHD2ZsrMYqQshXnMuomBodzPn+d0dDxWieTj232XCQ9QGt9fU9l5SDGC9hi8s24lDlg9FXPSI7T8A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17 || ^18 || ^19
|
||||
@@ -754,8 +754,8 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@base-ui-components/utils@0.1.1':
|
||||
resolution: {integrity: sha512-HWXZA8upEKgrdL1rQqxWu1H+2tB2cXzY2jCxvgnpUv3eoWN2jldhXxMZnXIjZF7jahGxSWXfSIM/qskiTWFFxA==}
|
||||
'@base-ui-components/utils@0.1.2':
|
||||
resolution: {integrity: sha512-aEitDGpMsYO2qnSpYOwZNykn9Rzn2ioyEVk2fyDRH7t+TIHVKpp9CeV7SPTq43M9mMSDxQ+7UeZJVkrj2dCVIQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17 || ^18 || ^19
|
||||
react: ^17 || ^18 || ^19
|
||||
@@ -2177,11 +2177,8 @@ packages:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
axios@1.10.0:
|
||||
resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
|
||||
|
||||
axios@1.11.0:
|
||||
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
|
||||
axios@1.12.2:
|
||||
resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
|
||||
|
||||
babel-plugin-react-compiler@19.1.0-rc.3:
|
||||
resolution: {integrity: sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==}
|
||||
@@ -2721,8 +2718,8 @@ packages:
|
||||
electron-to-chromium@1.5.162:
|
||||
resolution: {integrity: sha512-hQA+Zb5QQwoSaXJWEAGEw1zhk//O7qDzib05Z4qTqZfNju/FAkrm5ZInp0JbTp4Z18A6bilopdZWEYrFSsfllA==}
|
||||
|
||||
electron@37.2.1:
|
||||
resolution: {integrity: sha512-ae2EbzRNqIAHlftfCHtbbt6EgJUW8+zxWLONqNnn2iSrLF0O/pbxbff3xcpZYPpmFBs4uqjoi+s4QS7DQ+zZ/w==}
|
||||
electron@38.2.1:
|
||||
resolution: {integrity: sha512-P4pE2RpRg3kM8IeOK+heg6iAxR5wcXnNHrbVchn7M3GBnYAhjfJRkROusdOro5PlKzdtfKjesbbqaG4MqQXccg==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3069,10 +3066,6 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.2:
|
||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
form-data@4.0.3:
|
||||
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -4897,38 +4890,38 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
turbo-darwin-64@2.5.6:
|
||||
resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==}
|
||||
turbo-darwin-64@2.5.8:
|
||||
resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
turbo-darwin-arm64@2.5.6:
|
||||
resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==}
|
||||
turbo-darwin-arm64@2.5.8:
|
||||
resolution: {integrity: sha512-f1H/tQC9px7+hmXn6Kx/w8Jd/FneIUnvLlcI/7RGHunxfOkKJKvsoiNzySkoHQ8uq1pJnhJ0xNGTlYM48ZaJOQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
turbo-linux-64@2.5.6:
|
||||
resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==}
|
||||
turbo-linux-64@2.5.8:
|
||||
resolution: {integrity: sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
turbo-linux-arm64@2.5.6:
|
||||
resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==}
|
||||
turbo-linux-arm64@2.5.8:
|
||||
resolution: {integrity: sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
turbo-windows-64@2.5.6:
|
||||
resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==}
|
||||
turbo-windows-64@2.5.8:
|
||||
resolution: {integrity: sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
turbo-windows-arm64@2.5.6:
|
||||
resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==}
|
||||
turbo-windows-arm64@2.5.8:
|
||||
resolution: {integrity: sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
turbo@2.5.6:
|
||||
resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==}
|
||||
turbo@2.5.8:
|
||||
resolution: {integrity: sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w==}
|
||||
hasBin: true
|
||||
|
||||
type-check@0.4.0:
|
||||
@@ -5646,7 +5639,7 @@ snapshots:
|
||||
'@babel/core': 7.27.4
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/runtime@7.28.3': {}
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.22.15':
|
||||
dependencies:
|
||||
@@ -5732,10 +5725,10 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@base-ui-components/react@1.0.0-beta.3(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
'@base-ui-components/react@1.0.0-beta.4(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@base-ui-components/utils': 0.1.1(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@babel/runtime': 7.28.4
|
||||
'@base-ui-components/utils': 0.1.2(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@floating-ui/utils': 0.2.10
|
||||
react: 19.1.1
|
||||
@@ -5746,9 +5739,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.12
|
||||
|
||||
'@base-ui-components/utils@0.1.1(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
'@base-ui-components/utils@0.1.2(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@babel/runtime': 7.28.4
|
||||
'@floating-ui/utils': 0.2.10
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
@@ -7219,15 +7212,7 @@ snapshots:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
axios@1.10.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
form-data: 4.0.2
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axios@1.11.0:
|
||||
axios@1.12.2:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
form-data: 4.0.4
|
||||
@@ -7885,7 +7870,7 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.162: {}
|
||||
|
||||
electron@37.2.1:
|
||||
electron@38.2.1:
|
||||
dependencies:
|
||||
'@electron/get': 2.0.3
|
||||
'@types/node': 22.15.26
|
||||
@@ -8414,13 +8399,6 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.2:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@4.0.3:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
@@ -10391,32 +10369,32 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
turbo-darwin-64@2.5.6:
|
||||
turbo-darwin-64@2.5.8:
|
||||
optional: true
|
||||
|
||||
turbo-darwin-arm64@2.5.6:
|
||||
turbo-darwin-arm64@2.5.8:
|
||||
optional: true
|
||||
|
||||
turbo-linux-64@2.5.6:
|
||||
turbo-linux-64@2.5.8:
|
||||
optional: true
|
||||
|
||||
turbo-linux-arm64@2.5.6:
|
||||
turbo-linux-arm64@2.5.8:
|
||||
optional: true
|
||||
|
||||
turbo-windows-64@2.5.6:
|
||||
turbo-windows-64@2.5.8:
|
||||
optional: true
|
||||
|
||||
turbo-windows-arm64@2.5.6:
|
||||
turbo-windows-arm64@2.5.8:
|
||||
optional: true
|
||||
|
||||
turbo@2.5.6:
|
||||
turbo@2.5.8:
|
||||
optionalDependencies:
|
||||
turbo-darwin-64: 2.5.6
|
||||
turbo-darwin-arm64: 2.5.6
|
||||
turbo-linux-64: 2.5.6
|
||||
turbo-linux-arm64: 2.5.6
|
||||
turbo-windows-64: 2.5.6
|
||||
turbo-windows-arm64: 2.5.6
|
||||
turbo-darwin-64: 2.5.8
|
||||
turbo-darwin-arm64: 2.5.8
|
||||
turbo-linux-64: 2.5.8
|
||||
turbo-linux-arm64: 2.5.8
|
||||
turbo-windows-64: 2.5.8
|
||||
turbo-windows-arm64: 2.5.8
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
@@ -10673,7 +10651,7 @@ snapshots:
|
||||
|
||||
wait-on@7.2.0:
|
||||
dependencies:
|
||||
axios: 1.10.0
|
||||
axios: 1.12.2
|
||||
joi: 17.13.3
|
||||
lodash: 4.17.21
|
||||
minimist: 1.2.8
|
||||
|
||||
@@ -16,3 +16,4 @@ catalog:
|
||||
onlyBuiltDependencies:
|
||||
- electron
|
||||
- esbuild
|
||||
useNodeVersion: 22.15.1
|
||||
|
||||
+27
-27
@@ -1,12 +1,21 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalEnv": ["NODE_ENV", "GITHUB_TOKEN", "APPLEID", "APPLEIDPASS", "TEAMID", "CSC_KEY_PASSWORD", "CSC_LINK"],
|
||||
"globalEnv": [
|
||||
"NODE_ENV",
|
||||
"GITHUB_TOKEN",
|
||||
"APPLEID",
|
||||
"APPLEIDPASS",
|
||||
"TEAMID",
|
||||
"CSC_KEY_PASSWORD",
|
||||
"CSC_LINK",
|
||||
"SENTRY_AUTH_TOKEN"
|
||||
],
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"dev:server": {
|
||||
"dev:electron": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
@@ -14,45 +23,36 @@
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"dev:test": {
|
||||
"cache": false
|
||||
},
|
||||
|
||||
"test": {},
|
||||
"test:pipeline": {},
|
||||
|
||||
"lint": {
|
||||
"cache": false
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"typecheck": {
|
||||
"cache": false
|
||||
"dependsOn": ["^typecheck"]
|
||||
},
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"env": ["SENTRY_AUTH_TOKEN"]
|
||||
},
|
||||
"build:local": {},
|
||||
"build:electron": {
|
||||
"env": ["SENTRY_AUTH_TOKEN"]
|
||||
},
|
||||
"build:localdocker": {},
|
||||
"e2e": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", "build/**"]
|
||||
},
|
||||
|
||||
"dist-win": {
|
||||
"dependsOn": ["build"]
|
||||
"dependsOn": ["build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dist-mac": {
|
||||
"dependsOn": ["build"]
|
||||
"dependsOn": ["build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dist-mac:local": {
|
||||
"dependsOn": ["build"]
|
||||
"dependsOn": ["build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dist-linux": {
|
||||
"dependsOn": ["build"]
|
||||
},
|
||||
"cleanup": {}
|
||||
"dependsOn": ["build"],
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user