mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 18:03:47 +00:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de8f9ef587 | |||
| 217402050d | |||
| de75d55849 | |||
| c4e6215b29 | |||
| 57315595a5 | |||
| fc7ed6bd20 | |||
| 750927dc8d | |||
| 00d2ae0a7c | |||
| 56c4d68bae | |||
| a02cdf71c0 | |||
| 25030debe4 | |||
| 27243c4432 | |||
| 22559c59d4 | |||
| 700948bfbb | |||
| 960a39fbf4 | |||
| d986f438d9 | |||
| c2012bd9e3 | |||
| 1f0f92371f | |||
| 4b81725e80 | |||
| 846eaf569d | |||
| ccf1ddf2fd | |||
| 81e00993f2 | |||
| e22c7698c3 | |||
| ef053fc2f7 | |||
| 3b6d3a4631 | |||
| 31c3e603ed | |||
| 1dfe5c980d | |||
| 80e4dc0941 | |||
| bc0b83aa14 | |||
| 971c6c66f0 | |||
| 343697dc93 | |||
| 4058231d6b | |||
| c5ff254267 | |||
| ac14cb9624 | |||
| bcabc50ad1 | |||
| cbd7bce106 | |||
| 00aa8a09fa | |||
| d04f958c11 | |||
| 5716fc31cf | |||
| a5d628af0c | |||
| 111a1f95ba | |||
| 4bf74d86b3 | |||
| d2638d64e4 | |||
| 02cddeec4d | |||
| a8fe8b5f1e | |||
| 8d2db7ffcd | |||
| 313590905f | |||
| 7bbdab7dd9 | |||
| 304832ecde | |||
| 0ab4abfcfe | |||
| 1847822f31 | |||
| 4499495212 | |||
| 0035070db8 | |||
| 890a97d071 | |||
| 5441e5b07f | |||
| ab4b94f2d8 | |||
| fd7ccb4a5b | |||
| 2e6bf234d5 | |||
| c934b0ecdd | |||
| cd6d6f24eb | |||
| e632a08557 | |||
| 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 |
+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 |
@@ -13,20 +13,20 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- 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:
|
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
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -11,15 +11,20 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- 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:
|
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
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -53,15 +58,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -86,15 +85,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: Ontime Resolver build
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_resolver:
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
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:
|
||||||
|
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 build:resolver
|
||||||
|
|
||||||
|
- 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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- 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:
|
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
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Run code quality per package
|
# Run code quality
|
||||||
- name: React - Run linter + TypeScript checks
|
- name: Run linter
|
||||||
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: pnpm lint
|
run: pnpm lint
|
||||||
working-directory: ./packages/types
|
|
||||||
|
- name: Run TypeScript checks
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
# We choose to run tests separately
|
# We choose to run tests separately
|
||||||
- name: React - Run unit tests
|
- name: Run unit tests
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm test:pipeline
|
run: pnpm test
|
||||||
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
|
|
||||||
|
|
||||||
e2e-test:
|
e2e-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -70,21 +50,36 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- 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:
|
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
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build client
|
- name: Get installed Playwright version
|
||||||
run: pnpm build:local
|
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
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|||||||
+4
-1
@@ -7,4 +7,7 @@ playwright-report
|
|||||||
|
|
||||||
**/*.toml
|
**/*.toml
|
||||||
**/*.yml
|
**/*.yml
|
||||||
**/*.json
|
**/*.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
|
From the project root, run the following commands
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __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
|
### 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.
|
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
|
We do that by creating two terminals an running
|
||||||
- __Run the React UI__ by running `pnpm turbo dev --filter=ontime-ui`
|
- __Run the React UI__ by running `pnpm dev --filter=ontime-ui`
|
||||||
- __Run the nodejs server__ by running `pnpm turbo dev --filter=ontime-server`
|
- __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
|
## 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)
|
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.
|
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
|
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
|
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
|
- `pnpm e2e --headed` run tests with a visible browser window
|
||||||
|
|
||||||
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
## 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
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- __Build the UI and server__ by running `pnpm turbo run build:electron`
|
- __Build the UI and server__ by running `pnpm build`
|
||||||
- __Create the package__ by running `pnpm turbo run dist-win`, `pnpm turbo run dist-mac` or `pnpm turbo run dist-linux`
|
- __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`
|
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.
|
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
|
## DOCKER
|
||||||
|
|
||||||
|
|||||||
+36
-37
@@ -1,37 +1,36 @@
|
|||||||
FROM node:22-bullseye AS builder
|
FROM node:22-bullseye AS builder
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN npm install -g pnpm@10.11.0
|
RUN npm install -g pnpm@10.11.0
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils --filter=ontime-types install --config.dedupe-peer-dependents=false --frozen-lockfile
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
RUN pnpm run build:docker
|
||||||
|
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
# Environment Variable to signal that we are running production
|
# Environment Variable to signal that we are running production
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
# Ontime Data path
|
# Ontime Data path
|
||||||
ENV ONTIME_DATA=/data/
|
ENV ONTIME_DATA=/data/
|
||||||
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
# Prepare UI
|
# Prepare UI
|
||||||
COPY --from=builder /app/apps/client/build ./client/
|
COPY --from=builder /app/apps/client/build ./client/
|
||||||
|
|
||||||
# Prepare Backend
|
# Prepare Backend
|
||||||
COPY --from=builder /app/apps/server/dist/ ./server/
|
COPY --from=builder /app/apps/server/dist/ ./server/
|
||||||
COPY --from=builder /app/apps/server/src/external/ ./external/
|
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/user/ ./user/
|
||||||
COPY --from=builder /app/apps/server/src/html/ ./html/
|
COPY --from=builder /app/apps/server/src/html/ ./html/
|
||||||
|
|
||||||
# Export default ports
|
# Export default ports
|
||||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||||
|
|
||||||
CMD ["node", "server/docker.cjs"]
|
CMD ["node", "server/docker.cjs"]
|
||||||
|
|
||||||
# Build and run commands
|
# 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 buildx build . -t getontime/ontime
|
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ 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",
|
"name": "@getontime/cli",
|
||||||
"version": "4.0.0-beta.3",
|
"version": "4.0.0-beta.5",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -13,12 +13,6 @@
|
|||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<title>ontime</title>
|
<title>ontime</title>
|
||||||
<style>
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
background-color: #101010 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "4.0.0-beta.3",
|
"version": "4.0.0-beta.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"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/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"@tanstack/react-query-devtools": "^5.85.9",
|
"@tanstack/react-query-devtools": "^5.85.9",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.12.2",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||||
"csv-stringify": "^6.6.0",
|
"csv-stringify": "^6.6.0",
|
||||||
"prismjs": "^1.30.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",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
"postinstall": "pnpm addversion",
|
"postinstall": "pnpm addversion",
|
||||||
"dev": "cross-env BROWSER=none vite",
|
"dev": "cross-env BROWSER=none vite",
|
||||||
"build": "vite build",
|
"dev:electron": "pnpm dev",
|
||||||
"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",
|
|
||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"build": "vite build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:pipeline": "vitest run",
|
"test:pipeline": "vitest run",
|
||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build",
|
|
||||||
"analyse": "npx vite-bundle-visualizer"
|
"analyse": "npx vite-bundle-visualizer"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverla
|
|||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
import { ontimeQueryClient } from './common/queryClient';
|
import { ontimeQueryClient } from './common/queryClient';
|
||||||
import { connectSocket } from './common/utils/socket';
|
import { connectSocket } from './common/utils/socket';
|
||||||
|
import KeepAwake from './features/keep-awake/KeepAwake';
|
||||||
import { TranslationProvider } from './translation/TranslationProvider';
|
import { TranslationProvider } from './translation/TranslationProvider';
|
||||||
import AppRouter from './AppRouter';
|
import AppRouter from './AppRouter';
|
||||||
import { baseURI } from './externals';
|
import { baseURI } from './externals';
|
||||||
@@ -24,6 +25,7 @@ function App() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<TranslationProvider>
|
<TranslationProvider>
|
||||||
<IdentifyOverlay />
|
<IdentifyOverlay />
|
||||||
|
<KeepAwake />
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
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 { apiEntryUrl } from './constants';
|
||||||
import { createBlob, downloadBlob } from './utils';
|
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
|
* HTTP request to upload project file
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,15 +49,14 @@ class ErrorBoundary extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className={style.errorContainer} data-testid='error-container'>
|
<div className={style.errorContainer} data-testid='error-container'>
|
||||||
<div>
|
<div>
|
||||||
<p className={style.error}>:/</p>
|
<p className={style.error}>: /</p>
|
||||||
<p>Something went wrong</p>
|
<p>Something went wrong.</p>
|
||||||
<div
|
<a
|
||||||
role='button'
|
|
||||||
className={style.report}
|
className={style.report}
|
||||||
onClick={() => Sentry.showReportDialog({ eventId: this.state.eventId })}
|
href={`mailto:mail@getontime.no?subject=Error%20Report&body=${encodeURIComponent(this.reportContent)}`}
|
||||||
>
|
>
|
||||||
Report error
|
Report error
|
||||||
</div>
|
</a>
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
className={style.report}
|
className={style.report}
|
||||||
|
|||||||
@@ -3,24 +3,21 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
background-color: #121212;
|
background-color: $ui-black;
|
||||||
color: white;
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: $error-red;
|
color: $error-red;
|
||||||
font-weight: 600;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.report {
|
.report {
|
||||||
text-decoration: underline $error-red;
|
color: $blue-500;
|
||||||
cursor: pointer;
|
text-decoration: underline;
|
||||||
}
|
text-underline-offset: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.report:hover {
|
&:hover {
|
||||||
color: $error-red;
|
color: $ontime-color;
|
||||||
}
|
|
||||||
|
|
||||||
.report:active {
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MouseEvent } from 'react';
|
||||||
import { IoBan } from 'react-icons/io5';
|
import { IoBan } from 'react-icons/io5';
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
@@ -10,12 +11,13 @@ interface SwatchProps {
|
|||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Swatch(props: SwatchProps) {
|
export default function Swatch({ color, isSelected, onClick }: SwatchProps) {
|
||||||
const { color, isSelected, onClick } = props;
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
onClick?.(color);
|
onClick?.(color);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
||||||
|
|
||||||
if (!color) {
|
if (!color) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// avoid wrong submit on cancel
|
// avoid wrong submit on cancel
|
||||||
let ignoreChange = false;
|
const ignoreChangeRef = useRef(false);
|
||||||
|
|
||||||
// set internal value on duration change
|
// set internal value on duration change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -35,8 +35,8 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
* @param {string} newValue string to be parsed
|
* @param {string} newValue string to be parsed
|
||||||
*/
|
*/
|
||||||
const validateAndSubmit = (newValue: string) => {
|
const validateAndSubmit = (newValue: string) => {
|
||||||
if (ignoreChange) {
|
if (ignoreChangeRef.current) {
|
||||||
ignoreChange = false;
|
ignoreChangeRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
} else if (event.key === 'Tab') {
|
} else if (event.key === 'Tab') {
|
||||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
ignoreChange = true;
|
ignoreChangeRef.current = true;
|
||||||
setValue(millisToString(duration));
|
setValue(millisToString(duration));
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ interface AppLinkProps {
|
|||||||
* Component used to navigate to an editor link inside the same window
|
* Component used to navigate to an editor link inside the same window
|
||||||
* Handles the path to respect Ontime Clouds base URL
|
* Handles the path to respect Ontime Clouds base URL
|
||||||
*/
|
*/
|
||||||
export default function AppLink(props: PropsWithChildren<AppLinkProps>) {
|
export default function AppLink({ search, children }: PropsWithChildren<AppLinkProps>) {
|
||||||
const { search, children } = props;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleClick = () => navigate({ search });
|
const handleClick = () => navigate({ search });
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ interface ExternalLinkProps {
|
|||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExternalLink(props: ExternalLinkProps) {
|
export default function ExternalLink({ href, inline, children }: ExternalLinkProps) {
|
||||||
const { href, inline, children } = props;
|
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openLink(href);
|
openLink(href);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { IoClose, IoContract, IoExpand, 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 { useLocation } from 'react-router';
|
||||||
import { Dialog } from '@base-ui-components/react/dialog';
|
import { Dialog } from '@base-ui-components/react/dialog';
|
||||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||||
|
|
||||||
import { isLocalhost } from '../../../externals';
|
import { isLocalhost } from '../../../externals';
|
||||||
|
import { useKeepAwakeOptions } from '../../../features/keep-awake/KeepAwake';
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
@@ -31,6 +33,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
|||||||
const [isRenameOpen, handlers] = useDisclosure(false);
|
const [isRenameOpen, handlers] = useDisclosure(false);
|
||||||
const { fullscreen, toggle } = useFullscreen();
|
const { fullscreen, toggle } = useFullscreen();
|
||||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||||
|
const { keepAwake, toggleKeepAwake } = useKeepAwakeOptions();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,6 +65,13 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
|||||||
<IoSwapVertical />
|
<IoSwapVertical />
|
||||||
{mirror && <span className={style.note}>Active</span>}
|
{mirror && <span className={style.note}>Active</span>}
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
{window.isSecureContext && (
|
||||||
|
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
||||||
|
Keep Awake
|
||||||
|
<LuCoffee />
|
||||||
|
{keepAwake && <span className={style.note}>Active</span>}
|
||||||
|
</NavigationMenuItem>
|
||||||
|
)}
|
||||||
<NavigationMenuItem onClick={handlers.open}>Rename Client</NavigationMenuItem>
|
<NavigationMenuItem onClick={handlers.open}>Rename Client</NavigationMenuItem>
|
||||||
|
|
||||||
<hr className={style.separator} />
|
<hr className={style.separator} />
|
||||||
|
|||||||
@@ -62,8 +62,6 @@
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: $component-border-radius-md;
|
border-radius: $component-border-radius-md;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 20rem;
|
|
||||||
border: 1px solid $gray-1000;
|
border: 1px solid $gray-1000;
|
||||||
|
|
||||||
&[data-side='start'] {
|
&[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 {
|
.item {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|||||||
@@ -31,14 +31,17 @@ export default function Select<T>({ options, fluid, size = 'medium', ...selectRo
|
|||||||
<BaseSelect.Positioner side='bottom' align='start'>
|
<BaseSelect.Positioner side='bottom' align='start'>
|
||||||
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
||||||
<BaseSelect.Popup className={styles.popup}>
|
<BaseSelect.Popup className={styles.popup}>
|
||||||
{options.map(({ disabled, label, value }) => (
|
<BaseSelect.Arrow />
|
||||||
<BaseSelect.Item key={String(value)} className={styles.item} value={value} disabled={disabled}>
|
<BaseSelect.List className={styles.list}>
|
||||||
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
{options.map(({ disabled, label, value }) => (
|
||||||
<IoCheckmark className={styles.itemIndicatorIcon} />
|
<BaseSelect.Item key={String(value)} className={styles.item} value={value} disabled={disabled}>
|
||||||
</BaseSelect.ItemIndicator>
|
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
||||||
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
|
<IoCheckmark className={styles.itemIndicatorIcon} />
|
||||||
</BaseSelect.Item>
|
</BaseSelect.ItemIndicator>
|
||||||
))}
|
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
|
||||||
|
</BaseSelect.Item>
|
||||||
|
))}
|
||||||
|
</BaseSelect.List>
|
||||||
</BaseSelect.Popup>
|
</BaseSelect.Popup>
|
||||||
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
||||||
</BaseSelect.Positioner>
|
</BaseSelect.Positioner>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
width: 40rem;
|
width: 40rem;
|
||||||
|
max-width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
import { fetchCurrentRundown } from '../api/rundown';
|
import { fetchCurrentRundown } from '../api/rundown';
|
||||||
import { useSelectedEventId } from '../hooks/useSocket';
|
import { useSelectedEventId } from '../hooks/useSocket';
|
||||||
import { getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||||
|
|
||||||
import useProjectData from './useProjectData';
|
import useProjectData from './useProjectData';
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ export function useFlatRundown() {
|
|||||||
|
|
||||||
// update data whenever the revision changes
|
// update data whenever the revision changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.revision !== -1 && data.revision !== prevRevision) {
|
if (data.revision !== -1 || data.revision !== prevRevision) {
|
||||||
const flatRundown = data.flatOrder.map((id) => data.entries[id]);
|
const flatRundown = data.order.map((id) => data.entries[id]);
|
||||||
setFlatRundown(flatRundown);
|
setFlatRundown(flatRundown);
|
||||||
setPrevRevision(data.revision);
|
setPrevRevision(data.revision);
|
||||||
}
|
}
|
||||||
@@ -85,8 +85,8 @@ export function useFlatRundownWithMetadata() {
|
|||||||
/**
|
/**
|
||||||
* Provides access to a partial rundown based on a filter callback
|
* Provides access to a partial rundown based on a filter callback
|
||||||
*/
|
*/
|
||||||
export function usePartialRundown(cb: (event: OntimeEntry) => boolean) {
|
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
||||||
const { data, status } = useFlatRundown();
|
const { data, status } = useFlatRundownWithMetadata();
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return data.filter(cb);
|
return data.filter(cb);
|
||||||
}, [data, cb]);
|
}, [data, cb]);
|
||||||
|
|||||||
@@ -164,46 +164,15 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({
|
|||||||
timeDanger: state.eventNow?.timeDanger ?? null,
|
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useRundownOverview = createSelector((state: RuntimeStore) => ({
|
|
||||||
plannedStart: state.rundown.plannedStart,
|
|
||||||
actualStart: state.rundown.actualStart,
|
|
||||||
plannedEnd: state.rundown.plannedEnd,
|
|
||||||
expectedEnd: state.offset.expectedRundownEnd,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) => ({
|
|
||||||
playback: state.timer.playback,
|
|
||||||
clock: state.clock,
|
|
||||||
|
|
||||||
numEvents: state.rundown.numEvents,
|
|
||||||
selectedEventIndex: state.rundown.selectedEventIndex,
|
|
||||||
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
|
||||||
|
|
||||||
groupExpectedEnd: state.offset.expectedGroupEnd,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
|
||||||
clock: state.clock,
|
|
||||||
offset: state.offset.absolute,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
|
export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
|
||||||
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||||
mode: state.offset.mode,
|
mode: state.offset.mode,
|
||||||
currentDay: state.eventNow?.dayOffset ?? 0,
|
currentDay: state.rundown.currentDay ?? 0,
|
||||||
actualStart: state.rundown.actualStart,
|
actualStart: state.rundown.actualStart,
|
||||||
plannedStart: state.rundown.plannedStart,
|
plannedStart: state.rundown.plannedStart,
|
||||||
clock: state.clock,
|
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) => ({
|
export const usePing = createSelector((state: RuntimeStore) => ({
|
||||||
ping: state.ping,
|
ping: state.ping,
|
||||||
}));
|
}));
|
||||||
@@ -227,6 +196,45 @@ export const usePlayback = () => {
|
|||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ======================= Overview data subscriptions ======================= */
|
||||||
|
|
||||||
|
export const useStartTimesOverview = createSelector((state: RuntimeStore) => ({
|
||||||
|
plannedStart: state.rundown.plannedStart,
|
||||||
|
actualStart: state.rundown.actualStart,
|
||||||
|
plannedEnd: state.rundown.plannedEnd,
|
||||||
|
expectedEnd: state.offset.expectedRundownEnd,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useProgressOverview = createSelector((state: RuntimeStore) => ({
|
||||||
|
numEvents: state.rundown.numEvents,
|
||||||
|
selectedEventIndex: state.rundown.selectedEventIndex,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useOffsetOverview = createSelector((state: RuntimeStore) => ({
|
||||||
|
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||||
|
playback: state.timer.playback,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useGroupTimerOverView = createSelector((state: RuntimeStore) => ({
|
||||||
|
clock: state.clock,
|
||||||
|
mode: state.offset.mode,
|
||||||
|
groupExpectedEnd: state.offset.expectedGroupEnd,
|
||||||
|
// we can force these numbers to 0 fo this use case to avoid null checks
|
||||||
|
actualGroupStart: state.rundown.actualGroupStart ?? 0,
|
||||||
|
currentDay: state.eventNow?.dayOffset ?? 0,
|
||||||
|
playback: state.timer.playback,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useFlagTimerOverView = createSelector((state: RuntimeStore) => ({
|
||||||
|
clock: state.clock,
|
||||||
|
mode: state.offset.mode,
|
||||||
|
// we can force these numbers to 0 fo this use case to avoid null checks
|
||||||
|
actualStart: state.rundown.actualStart ?? 0,
|
||||||
|
plannedStart: state.rundown.plannedStart ?? 0,
|
||||||
|
currentDay: state.eventNow?.dayOffset ?? 0,
|
||||||
|
playback: state.timer.playback,
|
||||||
|
}));
|
||||||
|
|
||||||
/* ======================= View specific subscriptions ======================= */
|
/* ======================= View specific subscriptions ======================= */
|
||||||
|
|
||||||
export const useTimerSocket = createSelector((state: RuntimeStore) => ({
|
export const useTimerSocket = createSelector((state: RuntimeStore) => ({
|
||||||
@@ -268,11 +276,7 @@ export const useStudioTimersSocket = createSelector((state: RuntimeStore) => ({
|
|||||||
eventNow: state.eventNow,
|
eventNow: state.eventNow,
|
||||||
message: state.message,
|
message: state.message,
|
||||||
time: state.timer,
|
time: state.timer,
|
||||||
offset: state.offset,
|
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
|
||||||
rundown: state.rundown,
|
rundown: state.rundown,
|
||||||
}));
|
expectedRundownEnd: state.offset.expectedRundownEnd,
|
||||||
|
|
||||||
export const useTimelineSocket = createSelector((state: RuntimeStore) => ({
|
|
||||||
clock: state.clock,
|
|
||||||
offset: state.offset.absolute,
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export type Size = 'xs' | 'sm' | 'md' | 'lg';
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
type Target = 'cuesheet' | 'timer' | 'clock' | 'countdown' | 'backstage' | 'studio';
|
|
||||||
|
|
||||||
interface SelectionState {
|
|
||||||
[key: string]: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColumnPermissions {
|
|
||||||
read: string[];
|
|
||||||
write: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CuesheetLinksState {
|
|
||||||
target: Target | null;
|
|
||||||
readSelected: SelectionState;
|
|
||||||
writeSelected: SelectionState;
|
|
||||||
setTarget: (target: Target | null) => void;
|
|
||||||
setField: (field: 'read' | 'write', key: string, value: boolean) => void;
|
|
||||||
toggleField: (field: 'read' | 'write', key: string) => void;
|
|
||||||
selectAll: (field: 'read' | 'write', keys: string[]) => void;
|
|
||||||
clearAll: (field: 'read' | 'write', keys: string[]) => void;
|
|
||||||
// Returns arrays of column keys that have read/write permissions if target is 'cuesheet'
|
|
||||||
getSelections: () => ColumnPermissions | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCuesheetLinksStore = create<CuesheetLinksState>((set, get) => ({
|
|
||||||
target: null,
|
|
||||||
readSelected: {},
|
|
||||||
writeSelected: {},
|
|
||||||
setTarget: (target) => set({ target }),
|
|
||||||
setField: (field, key, value) =>
|
|
||||||
set((state) => ({
|
|
||||||
...(field === 'read'
|
|
||||||
? { readSelected: { ...state.readSelected, [key]: value } }
|
|
||||||
: { writeSelected: { ...state.writeSelected, [key]: value } }),
|
|
||||||
})),
|
|
||||||
toggleField: (field, key) =>
|
|
||||||
set((state) => ({
|
|
||||||
...(field === 'read'
|
|
||||||
? { readSelected: { ...state.readSelected, [key]: !state.readSelected[key] } }
|
|
||||||
: { writeSelected: { ...state.writeSelected, [key]: !state.writeSelected[key] } }),
|
|
||||||
})),
|
|
||||||
selectAll: (field, keys) =>
|
|
||||||
set((_state) => ({
|
|
||||||
...(field === 'read'
|
|
||||||
? {
|
|
||||||
readSelected: keys.reduce((acc, key) => {
|
|
||||||
acc[key] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as SelectionState),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
writeSelected: keys.reduce((acc, key) => {
|
|
||||||
acc[key] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as SelectionState),
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
clearAll: (field, keys) =>
|
|
||||||
set((_state) => ({
|
|
||||||
...(field === 'read'
|
|
||||||
? {
|
|
||||||
readSelected: keys.reduce((acc, key) => {
|
|
||||||
acc[key] = false;
|
|
||||||
return acc;
|
|
||||||
}, {} as SelectionState),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
writeSelected: keys.reduce((acc, key) => {
|
|
||||||
acc[key] = false;
|
|
||||||
return acc;
|
|
||||||
}, {} as SelectionState),
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
getSelections: () => {
|
|
||||||
const state = get();
|
|
||||||
if (state.target !== 'cuesheet') return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
read: Object.entries(state.readSelected)
|
|
||||||
.filter(([_, selected]) => selected)
|
|
||||||
.map(([key]) => key),
|
|
||||||
write: Object.entries(state.writeSelected)
|
|
||||||
.filter(([_, selected]) => selected)
|
|
||||||
.map(([key]) => key),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { OntimeEntry, ProjectRundowns, Rundown } from 'ontime-types';
|
import { makeCSVFromArrayOfArrays } from '../csv';
|
||||||
|
|
||||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../csv';
|
|
||||||
|
|
||||||
describe('makeCSVFromArrayOfArrays()', () => {
|
describe('makeCSVFromArrayOfArrays()', () => {
|
||||||
it('joins an array of arrays with commas and newlines', () => {
|
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 { stringify } from 'csv-stringify/browser/esm/sync';
|
||||||
import { OntimeEntry, ProjectRundowns } from 'ontime-types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an array of arrays to a CSV file
|
* 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 {
|
export function makeCSVFromArrayOfArrays(arrayOfArrays: string[][]): string {
|
||||||
return stringify(arrayOfArrays);
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { MaybeString } from 'ontime-types';
|
|
||||||
|
|
||||||
export default function safeParseNumber(value: MaybeString, defaultValue: number = 0): number {
|
|
||||||
if (!value) return defaultValue;
|
|
||||||
const number = Number(value);
|
|
||||||
if (isNaN(number)) return defaultValue;
|
|
||||||
return number;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ApiAction,
|
ApiActionTag,
|
||||||
Log,
|
Log,
|
||||||
MessageTag,
|
MessageTag,
|
||||||
RefetchKey,
|
RefetchKey,
|
||||||
@@ -199,7 +199,7 @@ export const connectSocket = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sendSocket<T extends MessageTag | ApiAction>(
|
export function sendSocket<T extends MessageTag | ApiActionTag>(
|
||||||
tag: T,
|
tag: T,
|
||||||
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
|
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
|
||||||
): void {
|
): void {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { APP_SETTINGS } from '../api/constants';
|
|||||||
import { useExpectedStartData } from '../hooks/useSocket';
|
import { useExpectedStartData } from '../hooks/useSocket';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
|
import { ExtendedEntry } from './rundownMetadata';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns current time in milliseconds from midnight
|
* Returns current time in milliseconds from midnight
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
@@ -154,3 +156,33 @@ export function useTimeUntilExpectedStart(
|
|||||||
);
|
);
|
||||||
return expectedStart - clock;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export function validateProjectFile(file: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Limit file size of a project file to around 1MB
|
// Limit file size of a project file to around 1MB
|
||||||
if (file.size > 1_000_000) {
|
if (file.size > 2_000_000) {
|
||||||
throw new Error('File size limit (1MB) exceeded');
|
throw new Error('File size limit (2MB) exceeded');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ export function validateLogo(file: File) {
|
|||||||
throw new Error('File is empty');
|
throw new Error('File is empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit file size of a project file to around 1MB
|
// Limit file size of a project file to around 1.5MB
|
||||||
if (file.size > 1_000_000) {
|
if (file.size > 1_500_000) {
|
||||||
throw new Error('File size limit (1MB) exceeded');
|
throw new Error('File size limit (1.5MB) exceeded');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ export const buyMeACoffeeUrl = 'https://buymeacoffee.com/cpvalente';
|
|||||||
|
|
||||||
// resolve environment
|
// resolve environment
|
||||||
export const appVersion = version;
|
export const appVersion = version;
|
||||||
export const isProduction = import.meta.env.MODE === 'production';
|
export const isDocker = import.meta.env.IS_DOCKER; // this env is made available by the vite.config.js define function
|
||||||
export const isDev = !isProduction;
|
export const isProduction = import.meta.env.PROD;
|
||||||
|
export const isDev = import.meta.env.DEV;
|
||||||
export const currentHostName = window.location.hostname;
|
export const currentHostName = window.location.hostname;
|
||||||
export const isLocalhost = currentHostName === 'localhost' || currentHostName === '127.0.0.1';
|
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');
|
export const isOntimeCloud = currentHostName.includes('cloud.getontime.no');
|
||||||
|
|
||||||
// resolve entrypoint URLs
|
// resolve entrypoint URLs
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
/* eslint-disable react/display-name */
|
|
||||||
import { ComponentType, useEffect } from 'react';
|
|
||||||
import { useLocation, useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
import useUrlPresets from '../common/hooks-query/useUrlPresets';
|
|
||||||
import { getRouteFromPreset } from '../common/utils/urlPresets';
|
|
||||||
|
|
||||||
const withPreset = <P extends object>(Component: ComponentType<P>) => {
|
|
||||||
return (props: Partial<P>) => {
|
|
||||||
const { data } = useUrlPresets();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
// navigate if is alias route
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return;
|
|
||||||
const destination = getRouteFromPreset(location, data);
|
|
||||||
|
|
||||||
// navigate to this destination if its not null
|
|
||||||
if (destination) {
|
|
||||||
navigate(destination);
|
|
||||||
}
|
|
||||||
}, [data, navigate, location]);
|
|
||||||
|
|
||||||
return <Component {...(props as P)} />;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withPreset;
|
|
||||||
@@ -107,16 +107,16 @@ export default function AutomationsList(props: AutomationsListProps) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</tr>
|
</tr>
|
||||||
{deleteError && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={5}>
|
|
||||||
<Panel.Error>{deleteError}</Panel.Error>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{deleteError && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5}>
|
||||||
|
<Panel.Error>{deleteError}</Panel.Error>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Panel.Table>
|
</Panel.Table>
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
|
|||||||
+6
-2
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { CustomFields, Rundown } from 'ontime-types';
|
import { CustomFields, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../../../../common/components/buttons/Button';
|
import Button from '../../../../../common/components/buttons/Button';
|
||||||
|
import useRundown from '../../../../../common/hooks-query/useRundown';
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import PreviewSpreadsheet from './preview/PreviewRundown';
|
import PreviewSpreadsheet from './preview/PreviewRundown';
|
||||||
@@ -17,7 +18,7 @@ interface ImportReviewProps {
|
|||||||
|
|
||||||
export default function ImportReview(props: ImportReviewProps) {
|
export default function ImportReview(props: ImportReviewProps) {
|
||||||
const { rundown, customFields, onFinished, onCancel } = props;
|
const { rundown, customFields, onFinished, onCancel } = props;
|
||||||
|
const { data: currentRundown } = useRundown();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { importRundown } = useGoogleSheet();
|
const { importRundown } = useGoogleSheet();
|
||||||
const resetPreview = useSheetStore((state) => state.resetPreview);
|
const resetPreview = useSheetStore((state) => state.resetPreview);
|
||||||
@@ -29,9 +30,12 @@ export default function ImportReview(props: ImportReviewProps) {
|
|||||||
|
|
||||||
const applyImport = async () => {
|
const applyImport = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
// we need to import on-top of the currently loaded rundown
|
||||||
|
// so the id needs to match
|
||||||
await importRundown(
|
await importRundown(
|
||||||
{
|
{
|
||||||
[rundown.id]: rundown,
|
[currentRundown.id]: { ...rundown, id: currentRundown.id, title: currentRundown.title },
|
||||||
},
|
},
|
||||||
customFields,
|
customFields,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MessageTag } from 'ontime-types';
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { usePing } from '../../../../common/hooks/useSocket';
|
import { usePing } from '../../../../common/hooks/useSocket';
|
||||||
import { sendSocket } from '../../../../common/utils/socket';
|
import { sendSocket } from '../../../../common/utils/socket';
|
||||||
import { isDockerImage } from '../../../../externals';
|
import { isDocker } from '../../../../externals';
|
||||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Network</Panel.Header>
|
<Panel.Header>Network</Panel.Header>
|
||||||
{isDockerImage && (
|
{isDocker && (
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<OntimeCloudStats />
|
<OntimeCloudStats />
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deleteProject,
|
deleteProject,
|
||||||
downloadCSV,
|
|
||||||
downloadProject,
|
downloadProject,
|
||||||
duplicateProject,
|
duplicateProject,
|
||||||
loadProject,
|
loadProject,
|
||||||
@@ -187,10 +186,6 @@ function ActionMenu(props: ActionMenuProps) {
|
|||||||
await downloadProject(filename);
|
await downloadProject(filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportCSV = async () => {
|
|
||||||
await downloadCSV(filename);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
render={<IconButton variant='ghosted-white' />}
|
render={<IconButton variant='ghosted-white' />}
|
||||||
@@ -213,7 +208,6 @@ function ActionMenu(props: ActionMenuProps) {
|
|||||||
{ type: 'item', icon: IoPencilOutline, label: 'Rename', onClick: handleRename },
|
{ type: 'item', icon: IoPencilOutline, label: 'Rename', onClick: handleRename },
|
||||||
{ type: 'item', icon: IoCopyOutline, label: 'Duplicate', onClick: handleDuplicate },
|
{ type: 'item', icon: IoCopyOutline, label: 'Duplicate', onClick: handleDuplicate },
|
||||||
{ type: 'item', icon: IoDocumentOutline, label: 'Download', onClick: handleDownload },
|
{ type: 'item', icon: IoDocumentOutline, label: 'Download', onClick: handleDownload },
|
||||||
{ type: 'item', icon: IoDocumentOutline, label: 'Export CSV Rundown', onClick: handleExportCSV },
|
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ type: 'item', icon: IoTrash, label: 'Delete', onClick: () => onDelete(filename), disabled: current },
|
{ type: 'item', icon: IoTrash, label: 'Delete', onClick: () => onDelete(filename), disabled: current },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default function ProjectData() {
|
|||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting, isValid, isDirty, errors },
|
formState: { isSubmitting, isValid, isDirty, errors },
|
||||||
setError,
|
setError,
|
||||||
|
clearErrors,
|
||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
@@ -53,6 +54,7 @@ export default function ProjectData() {
|
|||||||
|
|
||||||
const handleUploadProjectLogo = async (event: ChangeEvent<HTMLInputElement>) => {
|
const handleUploadProjectLogo = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
|
clearErrors('logo');
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
@@ -93,6 +95,7 @@ export default function ProjectData() {
|
|||||||
|
|
||||||
const onSubmit = async (formData: ProjectData) => {
|
const onSubmit = async (formData: ProjectData) => {
|
||||||
try {
|
try {
|
||||||
|
clearErrors();
|
||||||
await updateProjectData(formData);
|
await updateProjectData(formData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = maybeAxiosError(error);
|
const message = maybeAxiosError(error);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const staticOptions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'manage',
|
id: 'manage',
|
||||||
label: 'Project data',
|
label: 'Project settings',
|
||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'manage__defaults', label: 'Rundown defaults' },
|
{ id: 'manage__defaults', label: 'Rundown defaults' },
|
||||||
{ id: 'manage__custom', label: 'Custom fields' },
|
{ id: 'manage__custom', label: 'Custom fields' },
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { use, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
|
|
||||||
|
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
|
||||||
|
export default function KeepAwake() {
|
||||||
|
const { keepAwake } = useKeepAwakeOptions();
|
||||||
|
const [wakeLockSentinel, setWakeLockSentinel] = useState<WakeLockSentinel | null>(null);
|
||||||
|
|
||||||
|
const removeLock = () => {
|
||||||
|
if (wakeLockSentinel) wakeLockSentinel.release().finally(() => setWakeLockSentinel(null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const acquireLock = () => {
|
||||||
|
if (!wakeLockSentinel || wakeLockSentinel.released) {
|
||||||
|
setWakeLockSentinel(null);
|
||||||
|
navigator.wakeLock
|
||||||
|
.request('screen')
|
||||||
|
.then((sentinel) => {
|
||||||
|
setWakeLockSentinel(sentinel);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
if (keepAwake) {
|
||||||
|
acquireLock();
|
||||||
|
document.addEventListener(
|
||||||
|
'visibilitychange',
|
||||||
|
() => {
|
||||||
|
if (wakeLockSentinel !== null && document.visibilityState === 'visible') {
|
||||||
|
acquireLock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal: controller.signal },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
removeLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
removeLock();
|
||||||
|
};
|
||||||
|
}, [keepAwake]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keepAwakeKey = 'keep-awake';
|
||||||
|
|
||||||
|
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
|
||||||
|
// Helper to get value from either source, prioritizing defaultValues
|
||||||
|
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook exposes the keep awake options
|
||||||
|
*/
|
||||||
|
export function useKeepAwakeOptions() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const maybePreset = use(PresetContext);
|
||||||
|
|
||||||
|
const keepAwake = useMemo(() => {
|
||||||
|
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
|
||||||
|
return getOptionsFromParams(searchParams, defaultValues);
|
||||||
|
}, [maybePreset, searchParams]);
|
||||||
|
|
||||||
|
const toggleKeepAwake = useCallback(() => {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
if (keepAwake) {
|
||||||
|
searchParams.delete(keepAwakeKey);
|
||||||
|
} else {
|
||||||
|
searchParams.set(keepAwakeKey, '1');
|
||||||
|
}
|
||||||
|
return searchParams;
|
||||||
|
});
|
||||||
|
}, [keepAwake]);
|
||||||
|
|
||||||
|
return { keepAwake, toggleKeepAwake };
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { useSelectedEventId } from '../../common/hooks/useSocket';
|
|||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { throttle } from '../../common/utils/throttle';
|
import { throttle } from '../../common/utils/throttle';
|
||||||
@@ -21,14 +21,14 @@ import OperatorGroup from './operator-group/OperatorGroup';
|
|||||||
import StatusBar from './status-bar/StatusBar';
|
import StatusBar from './status-bar/StatusBar';
|
||||||
import { getOperatorOptions, useOperatorOptions } from './operator.options';
|
import { getOperatorOptions, useOperatorOptions } from './operator.options';
|
||||||
import type { EditEvent } from './operator.types';
|
import type { EditEvent } from './operator.types';
|
||||||
import { getEventData, makeOperatorMetadata } from './operator.utils';
|
import { getEventData } from './operator.utils';
|
||||||
|
|
||||||
import style from './Operator.module.scss';
|
import style from './Operator.module.scss';
|
||||||
|
|
||||||
const selectedOffset = 50;
|
const selectedOffset = 50;
|
||||||
|
|
||||||
export default function Operator() {
|
export default function Operator() {
|
||||||
const { data, status } = useRundown();
|
const { data, rundownMetadata, status } = useRundownWithMetadata();
|
||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
|
|
||||||
@@ -113,7 +113,6 @@ export default function Operator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canEdit = shouldEdit && subscribe.length;
|
const canEdit = shouldEdit && subscribe.length;
|
||||||
const { process } = makeOperatorMetadata(selectedEventId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.operatorContainer} data-testid='operator-view'>
|
<div className={style.operatorContainer} data-testid='operator-view'>
|
||||||
@@ -130,8 +129,7 @@ export default function Operator() {
|
|||||||
{data.order.map((entryId) => {
|
{data.order.map((entryId) => {
|
||||||
const entry = data.entries[entryId];
|
const entry = data.entries[entryId];
|
||||||
if (isOntimeEvent(entry)) {
|
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
|
// hide past events (if setting) and skipped events
|
||||||
if ((hidePast && isPast) || entry.skip) {
|
if ((hidePast && isPast) || entry.skip) {
|
||||||
return null;
|
return null;
|
||||||
@@ -158,9 +156,9 @@ export default function Operator() {
|
|||||||
delay={entry.delay}
|
delay={entry.delay}
|
||||||
dayOffset={entry.dayOffset}
|
dayOffset={entry.dayOffset}
|
||||||
isLinkedToLoaded={isLinkedToLoaded}
|
isLinkedToLoaded={isLinkedToLoaded}
|
||||||
isSelected={isSelected}
|
isSelected={isLoaded}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
selectedRef={isSelected ? selectedRef : undefined}
|
selectedRef={isLoaded ? selectedRef : undefined}
|
||||||
showStart={showStart}
|
showStart={showStart}
|
||||||
subscribed={subscribedData}
|
subscribed={subscribedData}
|
||||||
totalGap={totalGap}
|
totalGap={totalGap}
|
||||||
@@ -179,7 +177,7 @@ export default function Operator() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry);
|
const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[entryId];
|
||||||
|
|
||||||
// hide past events (if setting) and skipped events
|
// hide past events (if setting) and skipped events
|
||||||
if ((hidePast && isPast) || nestedEntry.skip) {
|
if ((hidePast && isPast) || nestedEntry.skip) {
|
||||||
@@ -207,9 +205,9 @@ export default function Operator() {
|
|||||||
delay={nestedEntry.delay}
|
delay={nestedEntry.delay}
|
||||||
dayOffset={nestedEntry.dayOffset}
|
dayOffset={nestedEntry.dayOffset}
|
||||||
isLinkedToLoaded={isLinkedToLoaded}
|
isLinkedToLoaded={isLinkedToLoaded}
|
||||||
isSelected={isSelected}
|
isSelected={isLoaded}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
selectedRef={isSelected ? selectedRef : undefined}
|
selectedRef={isLoaded ? selectedRef : undefined}
|
||||||
showStart={showStart}
|
showStart={showStart}
|
||||||
subscribed={subscribedData}
|
subscribed={subscribedData}
|
||||||
totalGap={totalGap}
|
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 (
|
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 }}>
|
<div className={style.binder} style={{ ...cueColours }}>
|
||||||
<span className={style.cue}>{cue}</span>
|
<span className={style.cue}>{cue}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,5 +167,5 @@ function TimeUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }:
|
|||||||
const isDue = timeUntil < MILLIS_PER_SECOND;
|
const isDue = timeUntil < MILLIS_PER_SECOND;
|
||||||
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`;
|
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 { CustomFields, MaybeString, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { getPropertyValue } from '../viewers/common/viewUtils';
|
import { getPropertyValue } from '../viewers/common/viewUtils';
|
||||||
|
|
||||||
import type { Subscribed } from './operator.types';
|
import type { Subscribed } from './operator.types';
|
||||||
|
|
||||||
type OperatorMetadata = {
|
export function getEventData(
|
||||||
isLinkedToLoaded: boolean;
|
event: OntimeEvent,
|
||||||
isPast: boolean;
|
main: MaybeString,
|
||||||
isSelected: boolean;
|
secondary: MaybeString,
|
||||||
totalGap: number;
|
subscriptions: string[],
|
||||||
};
|
customFields: CustomFields,
|
||||||
|
) {
|
||||||
export function makeOperatorMetadata(selectedId: EntryId | null) {
|
const mainField = main ? getPropertyValue(event, main) ?? '' : event.title;
|
||||||
const hasSelection = Boolean(selectedId);
|
const secondaryField = getPropertyValue(event, secondary) ?? '';
|
||||||
let hasSeenSelected = false;
|
|
||||||
let totalGap = 0;
|
// remove subscriptions that are not in customFields
|
||||||
/** if the event can link all the way back to the currently playing event */
|
const sanitisedSubscriptions = subscriptions.filter((field) => Object.hasOwn(customFields, field));
|
||||||
let isLinkedToLoaded = false;
|
const subscribedData = sanitisedSubscriptions.reduce<Subscribed>((acc, id) => {
|
||||||
let previousEvent: OntimeEvent | null = null;
|
const field = customFields[id];
|
||||||
|
if (field) {
|
||||||
function process(event: OntimeEvent): Readonly<OperatorMetadata> {
|
acc.push({
|
||||||
const isSelected = event.id === selectedId;
|
id,
|
||||||
if (isSelected) {
|
label: field.label,
|
||||||
hasSeenSelected = true;
|
colour: field.colour,
|
||||||
}
|
value: event.custom[id],
|
||||||
|
});
|
||||||
// is past if we havent yet seen the selected event
|
}
|
||||||
const isPast = hasSelection && !hasSeenSelected;
|
return acc;
|
||||||
totalGap += event.gap;
|
}, []);
|
||||||
|
|
||||||
if (!isPast && !isSelected) {
|
return { mainField, secondaryField, subscribedData };
|
||||||
/**
|
}
|
||||||
* 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 };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -47,9 +47,10 @@
|
|||||||
|
|
||||||
.daySpan {
|
.daySpan {
|
||||||
&::after {
|
&::after {
|
||||||
content: '*';
|
content: "+"attr(data-day-offset);
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
font-size: 0.75em;
|
font-size: 0.6em;
|
||||||
|
letter-spacing: 0;
|
||||||
color: $info-blue;
|
color: $info-blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,3 +68,10 @@
|
|||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dueTime {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: $playback-over;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,30 +8,33 @@ import {
|
|||||||
TbFolderPin,
|
TbFolderPin,
|
||||||
TbFolderStar,
|
TbFolderStar,
|
||||||
} from 'react-icons/tb';
|
} from 'react-icons/tb';
|
||||||
import { OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
|
import { OffsetMode, OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
|
||||||
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
import { dayInMs, isPlaybackActive, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||||
import {
|
import {
|
||||||
useClock,
|
useClock,
|
||||||
useCurrentGroupId,
|
useCurrentGroupId,
|
||||||
|
useFlagTimerOverView,
|
||||||
|
useGroupTimerOverView,
|
||||||
useNextFlag,
|
useNextFlag,
|
||||||
useRundownOverview,
|
useOffsetOverview,
|
||||||
useRuntimePlaybackOverview,
|
useProgressOverview,
|
||||||
|
useStartTimesOverview,
|
||||||
useTimer,
|
useTimer,
|
||||||
} from '../../../common/hooks/useSocket';
|
} from '../../../common/hooks/useSocket';
|
||||||
import { useEntry } from '../../../common/hooks-query/useRundown';
|
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||||
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
||||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { calculateEndAndDaySpan, formattedTime } from '../overview.utils';
|
import { calculateEndAndDaySpan, formatDueTime, formattedTime } from '../overview.utils';
|
||||||
|
|
||||||
import { OverUnder, TimeColumn } from './TimeLayout';
|
import { OverUnder, TimeColumn } from './TimeLayout';
|
||||||
|
|
||||||
import style from './TimeElements.module.scss';
|
import style from './TimeElements.module.scss';
|
||||||
|
|
||||||
export function StartTimes() {
|
export function StartTimes() {
|
||||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRundownOverview();
|
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useStartTimesOverview();
|
||||||
|
|
||||||
const plannedStartText = plannedStart === null ? timerPlaceholder : formatTime(plannedStart);
|
const plannedStartText = plannedStart === null ? timerPlaceholder : formatTime(plannedStart);
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ export function StartTimes() {
|
|||||||
<Tooltip text='Planned start time' render={<TbCalendarPin className={style.icon} />} />
|
<Tooltip text='Planned start time' render={<TbCalendarPin className={style.icon} />} />
|
||||||
<span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
|
<span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement} data-testid='actual-start-time'>
|
||||||
<Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} />
|
<Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} />
|
||||||
<span className={cx([style.time, actualStart === null && style.muted])}>{formattedTime(actualStart)}</span>
|
<span className={cx([style.time, actualStart === null && style.muted])}>{formattedTime(actualStart)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,8 +61,8 @@ export function StartTimes() {
|
|||||||
<Tooltip text='Planned end time' render={<TbCalendarPin className={style.icon} />} />
|
<Tooltip text='Planned end time' render={<TbCalendarPin className={style.icon} />} />
|
||||||
{maybePlannedDaySpan > 0 ? (
|
{maybePlannedDaySpan > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Event spans over ${maybePlannedDaySpan + 1} days`}
|
text={`Rundown spans over ${maybePlannedDaySpan + 1} days`}
|
||||||
render={<span className={cx([style.time, style.daySpan])} />}
|
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybePlannedDaySpan} />}
|
||||||
>
|
>
|
||||||
{plannedEndText}
|
{plannedEndText}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -71,8 +74,8 @@ export function StartTimes() {
|
|||||||
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
|
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
|
||||||
{maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? (
|
{maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Event spans over ${maybeExpectedDaySpan + 1} days`}
|
text={`Rundown spans over ${maybeExpectedDaySpan + 1} days`}
|
||||||
render={<span className={cx([style.time, style.daySpan])} />}
|
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybeExpectedDaySpan} />}
|
||||||
>
|
>
|
||||||
{formattedTime(maybeExpectedEnd)}
|
{formattedTime(maybeExpectedEnd)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -96,61 +99,110 @@ export function MetadataTimes() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: there a some things here we still need to think about, mainly what to do whit the planed group duration in relation to the events
|
|
||||||
function GroupTimes() {
|
function GroupTimes() {
|
||||||
const { clock, groupExpectedEnd } = useRuntimePlaybackOverview();
|
const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback } = useGroupTimerOverView();
|
||||||
const { currentGroupId } = useCurrentGroupId();
|
const { currentGroupId } = useCurrentGroupId();
|
||||||
const group = useEntry(currentGroupId) as OntimeGroup | null;
|
const group = useEntry(currentGroupId) as OntimeGroup | null;
|
||||||
|
|
||||||
// the group end time dose not encode any day offsets
|
const active = isPlaybackActive(playback);
|
||||||
const plannedGroupEnd = group && group.timeStart !== null ? group.timeStart + group.duration - clock : null;
|
|
||||||
const plannedTimeUntilGroupEnd = formattedTime(plannedGroupEnd, 3, TimerType.CountDown);
|
// the group end time dose not encode any day offsets so it is calculated with group start time and duration
|
||||||
|
const plannedGroupEnd = (() => {
|
||||||
|
if (!active) return null;
|
||||||
|
if (!group || group.timeStart === null) return null;
|
||||||
|
const normalizedClock = clock + currentDay * dayInMs;
|
||||||
|
|
||||||
|
return mode === OffsetMode.Absolute
|
||||||
|
? group.timeStart + group.duration - normalizedClock
|
||||||
|
: actualGroupStart + group.duration - normalizedClock;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const plannedTimeUntilGroupEnd = formatDueTime(plannedGroupEnd, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null;
|
const expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null;
|
||||||
const expectedTimeUntilGroupEnd = formattedTime(expectedGroupEnd, 3, TimerType.CountDown);
|
const expectedTimeUntilGroupEnd = formatDueTime(expectedGroupEnd, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const groupTitle = group?.title ?? null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.metadataRow}>
|
<div className={style.metadataRow}>
|
||||||
<span className={groupTitle ? style.labelTitle : style.label}>{`${groupTitle ? groupTitle : 'Group'} `}</span>
|
<span className={group?.title ? style.labelTitle : style.label}>{`${group?.title || 'Group'} `}</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to planned group end' render={<TbFolderPin className={style.icon} />} />
|
<Tooltip text='Time to planned group end' render={<TbFolderPin className={style.icon} />} />
|
||||||
<span className={cx([style.time, !group && style.muted])}>{plannedTimeUntilGroupEnd}</span>
|
<span
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
(!group || !active) && style.muted,
|
||||||
|
plannedTimeUntilGroupEnd === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{plannedTimeUntilGroupEnd}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to expected group end' render={<TbFolderStar className={style.icon} />} />
|
<Tooltip text='Time to expected group end' render={<TbFolderStar className={style.icon} />} />
|
||||||
<span className={cx([style.time, groupExpectedEnd === null && style.muted])}>{expectedTimeUntilGroupEnd}</span>
|
<span
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
!groupExpectedEnd && style.muted,
|
||||||
|
expectedTimeUntilGroupEnd === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{expectedTimeUntilGroupEnd}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlagTimes() {
|
function FlagTimes() {
|
||||||
const { clock } = useClock();
|
const { clock, mode, actualStart, plannedStart, playback, currentDay } = useFlagTimerOverView();
|
||||||
const { id, expectedStart } = useNextFlag();
|
const { id, expectedStart } = useNextFlag();
|
||||||
const entry = useEntry(id) as OntimeEvent | null;
|
const entry = useEntry(id) as OntimeEvent | null;
|
||||||
|
|
||||||
const plannedFlagStart = entry ? entry.timeStart - clock : null;
|
const active = isPlaybackActive(playback);
|
||||||
const plannedTimeUntilDisplay = formattedTime(plannedFlagStart, 3, TimerType.CountDown);
|
|
||||||
|
const plannedFlagStart = (() => {
|
||||||
|
if (!active) return null;
|
||||||
|
if (!entry) return null;
|
||||||
|
const normalizedTimeStart = entry.timeStart + entry.dayOffset * dayInMs;
|
||||||
|
const normalizedClock = clock + currentDay * dayInMs;
|
||||||
|
return mode === OffsetMode.Absolute
|
||||||
|
? normalizedTimeStart - normalizedClock
|
||||||
|
: normalizedTimeStart + actualStart - plannedStart - normalizedClock;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const plannedTimeUntilDisplay = formatDueTime(plannedFlagStart, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null;
|
const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null;
|
||||||
const expectedTimeUntilDisplay = formattedTime(expectedTimeUntil, 3, TimerType.CountDown);
|
const expectedTimeUntilDisplay = formatDueTime(expectedTimeUntil, 3, TimerType.CountDown);
|
||||||
|
|
||||||
const title = entry?.title ?? null;
|
const title = entry?.title ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.metadataRow}>
|
<div className={style.metadataRow}>
|
||||||
<span className={title ? style.labelTitle : style.label}>{`${title ? title : 'Flag'} `}</span>
|
<span className={title ? style.labelTitle : style.label}>{`${title || 'Flag'} `}</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to next flag planned start' render={<TbFlagPin className={style.icon} />} />
|
<Tooltip text='Time to next flag planned start' render={<TbFlagPin className={style.icon} />} />
|
||||||
<span data-testid='flag-plannedStart' className={cx([style.time, !entry && style.muted])}>
|
<span
|
||||||
|
data-testid='flag-plannedStart'
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
(!entry || !active) && style.muted,
|
||||||
|
plannedTimeUntilDisplay === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
{plannedTimeUntilDisplay}
|
{plannedTimeUntilDisplay}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
||||||
<span data-testid='flag-expectedStart' className={cx([style.time, expectedTimeUntil === null && style.muted])}>
|
<span
|
||||||
|
data-testid='flag-expectedStart'
|
||||||
|
className={cx([
|
||||||
|
style.time,
|
||||||
|
expectedTimeUntil === null && style.muted,
|
||||||
|
expectedTimeUntilDisplay === 'due' && style.dueTime,
|
||||||
|
])}
|
||||||
|
>
|
||||||
{expectedTimeUntilDisplay}
|
{expectedTimeUntilDisplay}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +211,7 @@ function FlagTimes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProgressOverview() {
|
export function ProgressOverview() {
|
||||||
const { numEvents, selectedEventIndex } = useRuntimePlaybackOverview();
|
const { numEvents, selectedEventIndex } = useProgressOverview();
|
||||||
|
|
||||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
||||||
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
|
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
|
||||||
@@ -168,7 +220,7 @@ export function ProgressOverview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function OffsetOverview() {
|
export function OffsetOverview() {
|
||||||
const { offset, playback } = useRuntimePlaybackOverview();
|
const { offset, playback } = useOffsetOverview();
|
||||||
|
|
||||||
const isPlaying = isPlaybackActive(playback);
|
const isPlaying = isPlaybackActive(playback);
|
||||||
const offsetState = getOffsetState(isPlaying ? offset : null);
|
const offsetState = getOffsetState(isPlaying ? offset : null);
|
||||||
|
|||||||
@@ -3,6 +3,23 @@ import { dayInMs, millisToString } from 'ontime-utils';
|
|||||||
|
|
||||||
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composition to stop negative timers from being formatted
|
||||||
|
* They should show a due string instead
|
||||||
|
*
|
||||||
|
* This is used for cases when a negative timer is unwanted
|
||||||
|
* eg: count down to a milestone
|
||||||
|
*/
|
||||||
|
export function formatDueTime(
|
||||||
|
time: MaybeNumber,
|
||||||
|
segments: number = 3,
|
||||||
|
direction?: TimerType.CountDown | TimerType.CountUp,
|
||||||
|
dueString = 'due',
|
||||||
|
): string {
|
||||||
|
if (time !== null && time <= 0) return dueString;
|
||||||
|
return formattedTime(time, segments, direction);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates the logic for formatting time in overview
|
* Encapsulates the logic for formatting time in overview
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
|
|||||||
|
|
||||||
const dragStyle = {
|
const dragStyle = {
|
||||||
zIndex: isDragging ? 2 : 'inherit',
|
zIndex: isDragging ? 2 : 'inherit',
|
||||||
cursor: isDragging ? 'grabbing' : 'grab',
|
|
||||||
transform: CSS.Translate.toString(transform),
|
transform: CSS.Translate.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -205,7 +205,6 @@ export default function RundownEvent({
|
|||||||
|
|
||||||
const dragStyle = {
|
const dragStyle = {
|
||||||
zIndex: isDragging ? 2 : 'inherit',
|
zIndex: isDragging ? 2 : 'inherit',
|
||||||
cursor: isDragging ? 'grabbing' : 'grab',
|
|
||||||
transform: CSS.Translate.toString(transform),
|
transform: CSS.Translate.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ interface EventUntilProps {
|
|||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventUntil(props: EventUntilProps) {
|
function EventUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }: EventUntilProps) {
|
||||||
const { timeStart, delay, dayOffset, totalGap, isLinkedToLoaded } = props;
|
|
||||||
|
|
||||||
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
|
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
|
||||||
const isDue = timeUntil < MILLIS_PER_SECOND;
|
const isDue = timeUntil < MILLIS_PER_SECOND;
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
|||||||
zIndex: isDragging ? 2 : 'inherit',
|
zIndex: isDragging ? 2 : 'inherit',
|
||||||
transform: CSS.Translate.toString(transform),
|
transform: CSS.Translate.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'grab',
|
cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'inherit',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
|||||||
|
|
||||||
const dragStyle = {
|
const dragStyle = {
|
||||||
zIndex: isDragging ? 2 : 'inherit',
|
zIndex: isDragging ? 2 : 'inherit',
|
||||||
cursor: isDragging ? 'grabbing' : 'grab',
|
|
||||||
transform: CSS.Translate.toString(transform),
|
transform: CSS.Translate.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
|||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
name='lockNav'
|
name='lockNav'
|
||||||
|
data-testid='lockNav'
|
||||||
checked={watch('lockNav')}
|
checked={watch('lockNav')}
|
||||||
onCheckedChange={(checked) => setValue('lockNav', checked, { shouldDirty: true })}
|
onCheckedChange={(checked) => setValue('lockNav', checked, { shouldDirty: true })}
|
||||||
disabled={watch('lockConfig')}
|
disabled={watch('lockConfig')}
|
||||||
@@ -239,6 +240,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
|||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
name='lockConfig'
|
name='lockConfig'
|
||||||
|
data-testid='lockConfig'
|
||||||
checked={watch('lockConfig')}
|
checked={watch('lockConfig')}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
@@ -254,6 +256,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
|||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
name='authenticate'
|
name='authenticate'
|
||||||
|
data-testid='authenticate'
|
||||||
checked={watch('authenticate')}
|
checked={watch('authenticate')}
|
||||||
onCheckedChange={(checked) => setValue('authenticate', checked, { shouldDirty: true })}
|
onCheckedChange={(checked) => setValue('authenticate', checked, { shouldDirty: true })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ body,
|
|||||||
html {
|
html {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-family: $ontime-font-family;
|
font-family: $ontime-font-family;
|
||||||
background-color: var(--background-color-override, $ui-black);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ $header-font-size: clamp(24px, 2.5vw, 48px);
|
|||||||
|
|
||||||
// General styling
|
// General styling
|
||||||
$accent-color: $red-500; // --accent-color-override
|
$accent-color: $red-500; // --accent-color-override
|
||||||
$delay-color: $ontime-delay-text;
|
|
||||||
$viewer-label-color: rgba(white, 25%);
|
$viewer-label-color: rgba(white, 25%);
|
||||||
|
|
||||||
// Main Properties of a viewer
|
// Main Properties of a viewer
|
||||||
@@ -43,4 +42,4 @@ $timer-bold-font-family: 'Arial Black', sans-serif; // --font-family-bold-overri
|
|||||||
$external-color: rgba(white, 85%); // --external-color-override
|
$external-color: rgba(white, 85%); // --external-color-override
|
||||||
|
|
||||||
// properties related to the studio clock
|
// properties related to the studio clock
|
||||||
$studio-breakpoint: 1200px;
|
$studio-breakpoint: 1300px;
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import {
|
|||||||
makeProjectDataOptions,
|
makeProjectDataOptions,
|
||||||
} from '../../common/components/view-params-editor/viewParams.utils';
|
} from '../../common/components/view-params-editor/viewParams.utils';
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import { scheduleOptions } from '../common/schedule/schedule.options';
|
import { getScheduleOptions } from '../common/schedule/schedule.options';
|
||||||
|
|
||||||
export const getBackstageOptions = (
|
export const getBackstageOptions = (
|
||||||
timeFormat: string,
|
timeFormat: string,
|
||||||
customFields: CustomFields,
|
customFields: CustomFields,
|
||||||
projectData: ProjectData,
|
projectData: ProjectData,
|
||||||
): ViewOption[] => {
|
): ViewOption[] => {
|
||||||
|
const customFieldOptions = makeOptionsFromCustomFields(customFields, []);
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
{ value: 'none', label: 'None' },
|
{ value: 'none', label: 'None' },
|
||||||
{ value: 'note', label: 'Note' },
|
{ value: 'note', label: 'Note' },
|
||||||
@@ -39,7 +40,7 @@ export const getBackstageOptions = (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
scheduleOptions,
|
getScheduleOptions(customFieldOptions),
|
||||||
{
|
{
|
||||||
title: OptionTitle.ElementVisibility,
|
title: OptionTitle.ElementVisibility,
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
import { getScheduledTimes } from './schedule.utils';
|
|
||||||
import { useSchedule } from './ScheduleContext';
|
import { useSchedule } from './ScheduleContext';
|
||||||
import ScheduleItem from './ScheduleItem';
|
import ScheduleItem from './ScheduleItem';
|
||||||
|
|
||||||
@@ -20,17 +19,21 @@ export default function Schedule({ className }: ScheduleProps) {
|
|||||||
return (
|
return (
|
||||||
<ul className={cx(['schedule', className])} ref={containerRef}>
|
<ul className={cx(['schedule', className])} ref={containerRef}>
|
||||||
{events.map((event) => {
|
{events.map((event) => {
|
||||||
const { timeStart, timeEnd, delay } = getScheduledTimes(event);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScheduleItem
|
<ScheduleItem
|
||||||
key={event.id}
|
key={event.id}
|
||||||
timeStart={timeStart}
|
timeStart={event.timeStart}
|
||||||
timeEnd={timeEnd}
|
dayOffset={event.dayOffset}
|
||||||
title={event.title}
|
delay={event.delay}
|
||||||
|
totalGap={event.totalGap}
|
||||||
|
isLinkedToLoaded={event.isLinkedToLoaded}
|
||||||
|
countToEnd={event.countToEnd}
|
||||||
|
duration={event.duration}
|
||||||
colour={event.colour}
|
colour={event.colour}
|
||||||
skip={event.skip}
|
skip={event.skip}
|
||||||
delay={delay}
|
title={event.title}
|
||||||
|
timeEnd={event.timeEnd}
|
||||||
|
cue={event.cue}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import {
|
import { createContext, PropsWithChildren, RefObject, use, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
createContext,
|
import { EntryId, isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||||
PropsWithChildren,
|
|
||||||
RefObject,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
|
||||||
|
|
||||||
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
||||||
|
import { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
|
|
||||||
import { useScheduleOptions } from './schedule.options';
|
import { useScheduleOptions } from './schedule.options';
|
||||||
|
|
||||||
interface ScheduleContextState {
|
interface ScheduleContextState {
|
||||||
events: OntimeEvent[];
|
events: ExtendedEntry<OntimeEvent>[];
|
||||||
selectedEventId: string | null;
|
selectedEventId: string | null;
|
||||||
numPages: number;
|
numPages: number;
|
||||||
visiblePage: number;
|
visiblePage: number;
|
||||||
@@ -25,13 +17,18 @@ interface ScheduleContextState {
|
|||||||
const ScheduleContext = createContext<ScheduleContextState | undefined>(undefined);
|
const ScheduleContext = createContext<ScheduleContextState | undefined>(undefined);
|
||||||
|
|
||||||
interface ScheduleProviderProps {
|
interface ScheduleProviderProps {
|
||||||
selectedEventId: string | null;
|
selectedEventId: EntryId | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
|
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
|
||||||
const { cycleInterval, stopCycle } = useScheduleOptions();
|
const { cycleInterval, stopCycle, filter } = useScheduleOptions();
|
||||||
const { data: events } = usePartialRundown((event: OntimeEntry) => {
|
const { data: events } = usePartialRundown((entry: ExtendedEntry<OntimeEntry>) => {
|
||||||
return isOntimeEvent(event);
|
if (filter) {
|
||||||
|
// custom keys are prepended with custom-
|
||||||
|
const customKey = filter.startsWith('custom-') ? filter.slice('custom-'.length) : filter;
|
||||||
|
return isOntimeEvent(entry) && Boolean(entry.custom[customKey]);
|
||||||
|
}
|
||||||
|
return isOntimeEvent(entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [firstIndex, setFirstIndex] = useState(-1);
|
const [firstIndex, setFirstIndex] = useState(-1);
|
||||||
@@ -135,9 +132,9 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
|
|||||||
selectedEventIndex = 0;
|
selectedEventIndex = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScheduleContext.Provider
|
<ScheduleContext
|
||||||
value={{
|
value={{
|
||||||
events: viewEvents as OntimeEvent[],
|
events: viewEvents as ExtendedEntry<OntimeEvent>[],
|
||||||
selectedEventId,
|
selectedEventId,
|
||||||
numPages,
|
numPages,
|
||||||
visiblePage,
|
visiblePage,
|
||||||
@@ -145,12 +142,12 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScheduleContext.Provider>
|
</ScheduleContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSchedule = () => {
|
export const useSchedule = () => {
|
||||||
const context = useContext(ScheduleContext);
|
const context = use(ScheduleContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useSchedule() can only be used inside a ScheduleContext');
|
throw new Error('useSchedule() can only be used inside a ScheduleContext');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { getOffsetState } from '../../../common/utils/offset';
|
||||||
|
import { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
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 SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import { useScheduleOptions } from './schedule.options';
|
import { useScheduleOptions } from './schedule.options';
|
||||||
@@ -13,116 +16,150 @@ const formatOptions = {
|
|||||||
format24: 'HH:mm',
|
format24: 'HH:mm',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ScheduleItemProps {
|
type ScheduleItemProps = Pick<
|
||||||
timeStart: number;
|
ExtendedEntry<OntimeEvent>,
|
||||||
timeEnd: number;
|
| 'timeStart'
|
||||||
title: string;
|
| 'dayOffset'
|
||||||
colour?: string;
|
| 'delay'
|
||||||
skip?: boolean;
|
| 'totalGap'
|
||||||
delay: number;
|
| 'isLinkedToLoaded'
|
||||||
}
|
| 'countToEnd'
|
||||||
|
| 'duration'
|
||||||
|
| 'colour'
|
||||||
|
| 'skip'
|
||||||
|
| 'title'
|
||||||
|
| 'timeEnd'
|
||||||
|
| 'cue'
|
||||||
|
>;
|
||||||
|
|
||||||
export default function ScheduleItem(props: ScheduleItemProps) {
|
export default function ScheduleItem({
|
||||||
const { timeStart, timeEnd, title, colour, skip, delay } = props;
|
timeStart,
|
||||||
|
dayOffset,
|
||||||
|
delay,
|
||||||
|
totalGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
countToEnd,
|
||||||
|
colour,
|
||||||
|
duration,
|
||||||
|
skip,
|
||||||
|
title,
|
||||||
|
timeEnd,
|
||||||
|
cue,
|
||||||
|
}: ScheduleItemProps) {
|
||||||
const { showExpected } = useScheduleOptions();
|
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 (
|
return (
|
||||||
<li className={cx(['entry', skip && 'entry--skip'])}>
|
<li className={cx(['entry', skip && 'entry--skip'])} data-testid={cue}>
|
||||||
<div className='entry-times'>
|
<div className='entry-times'>
|
||||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
{showExpected ? (
|
||||||
<SuperscriptTime time={start} />
|
<ExpectedScheduleItem
|
||||||
→
|
timeStart={timeStart}
|
||||||
<SuperscriptTime time={end} />
|
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>
|
||||||
<div className='entry-title'>{title}</div>
|
<div className='entry-title'>{title}</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DelayedScheduleItem(props: ScheduleItemProps) {
|
function PlannedScheduleItem({
|
||||||
const { timeStart, timeEnd, title, colour, skip, delay } = props;
|
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 start = formatTime(timeStart, formatOptions);
|
||||||
const end = formatTime(timeEnd, formatOptions);
|
const end = formatTime(timeEnd, formatOptions);
|
||||||
const delayedStart = formatTime(timeStart + delay, formatOptions);
|
const delayedStart = formatTime(timeStart + delay, formatOptions);
|
||||||
const delayedEnd = formatTime(timeEnd + delay, formatOptions);
|
const delayedEnd = formatTime(timeEnd + delay, formatOptions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={cx(['entry', skip && 'entry--skip'])}>
|
<>
|
||||||
<div className='entry-times'>
|
<span className='entry-times--delayed'>
|
||||||
<span className='entry-times--delayed'>
|
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
<SuperscriptTime time={start} />
|
||||||
<SuperscriptTime time={start} />
|
→
|
||||||
→
|
<SuperscriptTime time={end} />
|
||||||
<SuperscriptTime time={end} />
|
</span>
|
||||||
</span>
|
<span className='entry-times--delay'>
|
||||||
<span className='entry-times--delay'>
|
<SuperscriptTime time={delayedStart} />
|
||||||
<SuperscriptTime time={delayedStart} />
|
→
|
||||||
→
|
<SuperscriptTime time={delayedEnd} />
|
||||||
<SuperscriptTime time={delayedEnd} />
|
</span>
|
||||||
</span>
|
</>
|
||||||
</div>
|
|
||||||
<div className='entry-title'>{title}</div>
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExpectedScheduleItem(props: ScheduleItemProps) {
|
function ExpectedScheduleItem({
|
||||||
const { timeStart, timeEnd, title, colour, skip, delay } = props;
|
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 (
|
return (
|
||||||
<li className={cx(['entry', skip && 'entry--skip'])}>
|
<>
|
||||||
<div className='entry-times'>
|
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
||||||
<span className='entry-colour' style={{ backgroundColor: colour }} />
|
<ExpectedTime expectedTime={expectedStart} plannedTime={timeStart} />
|
||||||
<ExpectedTime time={timeStart} delay={delay} />
|
→
|
||||||
→
|
<ExpectedTime expectedTime={expectedEnd} plannedTime={plannedEnd} />
|
||||||
<ExpectedTime time={timeEnd} delay={delay} />
|
</>
|
||||||
</div>
|
|
||||||
<div className='entry-title'>{title}</div>
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExpectedTimeProps {
|
interface ExpectedTimeProps {
|
||||||
time: number;
|
expectedTime: number;
|
||||||
delay: number;
|
plannedTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExpectedTime(props: ExpectedTimeProps) {
|
function ExpectedTime({ expectedTime, plannedTime }: ExpectedTimeProps) {
|
||||||
const { time, delay } = props;
|
const timeDisplay = formatTime(expectedTime);
|
||||||
const { offset } = useRuntimeOffset();
|
const expectedState = getOffsetState(expectedTime - plannedTime);
|
||||||
|
return <SuperscriptTime className={`entry-times--${expectedState}`} time={timeDisplay} />;
|
||||||
const expectedOffset = offset - delay;
|
|
||||||
const expectedTime = formatTime(time - offset, formatOptions);
|
|
||||||
const expectedState = getOffsetState(expectedOffset);
|
|
||||||
|
|
||||||
return <SuperscriptTime className={`entry-times--${expectedState}`} time={expectedTime} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { SelectOption } from '../../../common/components/select/Select';
|
||||||
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||||
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
import type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
export const scheduleOptions: ViewOption = {
|
export const getScheduleOptions = (customFieldOptions: SelectOption[]): ViewOption => ({
|
||||||
title: OptionTitle.Schedule,
|
title: OptionTitle.Schedule,
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
id: 'filter',
|
||||||
|
title: 'Filter',
|
||||||
|
description: 'Hide events without data in the selected custom field',
|
||||||
|
type: 'option',
|
||||||
|
values: customFieldOptions,
|
||||||
|
defaultValue: 'None',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'stopCycle',
|
id: 'stopCycle',
|
||||||
title: 'Stop cycling through event pages',
|
title: 'Stop cycling through event pages',
|
||||||
@@ -31,9 +40,10 @@ export const scheduleOptions: ViewOption = {
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
|
|
||||||
type ScheduleOptions = {
|
type ScheduleOptions = {
|
||||||
|
filter: string | null;
|
||||||
cycleInterval: number;
|
cycleInterval: number;
|
||||||
stopCycle: boolean;
|
stopCycle: boolean;
|
||||||
showExpected: boolean;
|
showExpected: boolean;
|
||||||
@@ -41,6 +51,7 @@ type ScheduleOptions = {
|
|||||||
|
|
||||||
function getScheduleOptionsFromParams(searchParams: URLSearchParams): ScheduleOptions {
|
function getScheduleOptionsFromParams(searchParams: URLSearchParams): ScheduleOptions {
|
||||||
return {
|
return {
|
||||||
|
filter: searchParams.get('filter'),
|
||||||
cycleInterval: Number(searchParams.get('cycleInterval')) || 10,
|
cycleInterval: Number(searchParams.get('cycleInterval')) || 10,
|
||||||
stopCycle: isStringBoolean(searchParams.get('stopCycle')),
|
stopCycle: isStringBoolean(searchParams.get('stopCycle')),
|
||||||
showExpected: isStringBoolean(searchParams.get('showExpected')),
|
showExpected: isStringBoolean(searchParams.get('showExpected')),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -141,7 +141,7 @@ $item-height: 3.5rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sub__schedule--delayed {
|
.sub__schedule--delayed {
|
||||||
color: $delay-color;
|
color: $ontime-delay-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub__schedule--strike {
|
.sub__schedule--strike {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types';
|
import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../common/components/buttons/Button';
|
import Button from '../../common/components/buttons/Button';
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
@@ -16,7 +16,7 @@ import { useTranslation } from '../../translation/TranslationProvider';
|
|||||||
import Loader from '../common/loader/Loader';
|
import Loader from '../common/loader/Loader';
|
||||||
|
|
||||||
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
||||||
import { CountdownSubscription, getOrderedSubscriptions } from './countdown.utils';
|
import { getOrderedSubscriptions } from './countdown.utils';
|
||||||
import CountdownSelect from './CountdownSelect';
|
import CountdownSelect from './CountdownSelect';
|
||||||
import CountdownSubscriptions from './CountdownSubscriptions';
|
import CountdownSubscriptions from './CountdownSubscriptions';
|
||||||
import SingleEventCountdown from './SingleEventCountdown';
|
import SingleEventCountdown from './SingleEventCountdown';
|
||||||
@@ -59,8 +59,6 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
|
|||||||
[defaultFormat, customFields, subscriptions],
|
[defaultFormat, customFields, subscriptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(subscriptions)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||||
<ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} />
|
<ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} />
|
||||||
@@ -89,7 +87,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
|
|||||||
|
|
||||||
interface CountdownContentsProps {
|
interface CountdownContentsProps {
|
||||||
playableEvents: ExtendedEntry<OntimeEvent>[];
|
playableEvents: ExtendedEntry<OntimeEvent>[];
|
||||||
subscriptions: CountdownSubscription;
|
subscriptions: EntryId[];
|
||||||
goToEditMode: () => void;
|
goToEditMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoArrowBack, IoClose, IoSaveOutline, IoAlbumsOutline } from 'react-icons/io5';
|
import { IoArrowBack, IoClose, IoSaveOutline } from 'react-icons/io5';
|
||||||
import { useNavigate } from 'react-router';
|
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 Button from '../../common/components/buttons/Button';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
|
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
|
||||||
|
|
||||||
import { CountdownSubscription, makeSubscriptionsUrl } from './countdown.utils';
|
import { makeSubscriptionsUrl } from './countdown.utils';
|
||||||
|
|
||||||
import './Countdown.scss';
|
import './Countdown.scss';
|
||||||
|
|
||||||
interface CountdownSelectProps {
|
interface CountdownSelectProps {
|
||||||
events: OntimeEvent[];
|
events: PlayableEvent[];
|
||||||
subscriptions: CountdownSubscription;
|
subscriptions: EntryId[];
|
||||||
disableEdit: () => void;
|
disableEdit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CountdownSelect({ events, subscriptions, disableEdit }: CountdownSelectProps) {
|
export default function CountdownSelect({ events, subscriptions, disableEdit }: CountdownSelectProps) {
|
||||||
const maybeAllSubscriptions: EntryId[] = subscriptions === 'all' ? events.map((event) => event.id) : subscriptions;
|
const [selected, setSelected] = useState<EntryId[]>(subscriptions);
|
||||||
const [selected, setSelected] = useState<EntryId[]>(maybeAllSubscriptions);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,24 +47,12 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
|
|||||||
navigate(url.search.toString());
|
navigate(url.search.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a URL with all
|
|
||||||
* and navigates to it
|
|
||||||
*/
|
|
||||||
const applyAll = () => {
|
|
||||||
// we remove events that no longer exist to avoid stale subscriptions
|
|
||||||
const url = makeSubscriptionsUrl(window.location.href, 'all');
|
|
||||||
disableEdit();
|
|
||||||
setSelected([]);
|
|
||||||
navigate(url.search.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
// make a copy of the selected array for quick lookup
|
// make a copy of the selected array for quick lookup
|
||||||
const selectedIds = new Set(selected);
|
const selectedIds = new Set(selected);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='list-container'>
|
<div className='list-container'>
|
||||||
{events.map((event: OntimeEvent, index: number) => {
|
{events.map((event, index) => {
|
||||||
const title = event.title || '{no title}';
|
const title = event.title || '{no title}';
|
||||||
const isSelected = selectedIds.has(event.id);
|
const isSelected = selectedIds.has(event.id);
|
||||||
|
|
||||||
@@ -99,10 +86,6 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
|
|||||||
<Button variant='subtle' size='xlarge' onClick={disableEdit}>
|
<Button variant='subtle' size='xlarge' onClick={disableEdit}>
|
||||||
<IoArrowBack /> Go back
|
<IoArrowBack /> Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='subtle' size='xlarge' onClick={applyAll}>
|
|
||||||
{/* TODO: icon ??? */}
|
|
||||||
<IoAlbumsOutline /> Use All
|
|
||||||
</Button>
|
|
||||||
<Button variant='subtle' size='xlarge' onClick={() => setSelected([])} disabled={selected.length === 0}>
|
<Button variant='subtle' size='xlarge' onClick={() => setSelected([])} disabled={selected.length === 0}>
|
||||||
<IoClose /> Clear
|
<IoClose /> Clear
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
|
|||||||
key={event.id}
|
key={event.id}
|
||||||
ref={isLive ? selectedRef : undefined}
|
ref={isLive ? selectedRef : undefined}
|
||||||
className={cx(['sub', isLive && 'sub--live', isArmed && 'sub--armed'])}
|
className={cx(['sub', isLive && 'sub--live', isArmed && 'sub--armed'])}
|
||||||
|
data-testid={event.cue}
|
||||||
>
|
>
|
||||||
<div className='sub__binder' style={{ '--user-color': event.colour }} />
|
<div className='sub__binder' style={{ '--user-color': event.colour }} />
|
||||||
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
|
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import { ViewOption } from '../../common/components/view-params-editor/viewParam
|
|||||||
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||||
import { CountdownSubscription } from './countdown.utils';
|
|
||||||
|
|
||||||
export const getCountdownOptions = (
|
export const getCountdownOptions = (
|
||||||
timeFormat: string,
|
timeFormat: string,
|
||||||
customFields: CustomFields,
|
customFields: CustomFields,
|
||||||
persistedSubscriptions: CountdownSubscription,
|
persistedSubscriptions: EntryId[],
|
||||||
): ViewOption[] => {
|
): ViewOption[] => {
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
{ value: 'none', label: 'None' },
|
{ value: 'none', label: 'None' },
|
||||||
@@ -56,7 +55,7 @@ export const getCountdownOptions = (
|
|||||||
id: 'sub',
|
id: 'sub',
|
||||||
title: 'Event subscription',
|
title: 'Event subscription',
|
||||||
description: 'The events to follow',
|
description: 'The events to follow',
|
||||||
values: persistedSubscriptions === 'all' ? ['all'] : persistedSubscriptions,
|
values: persistedSubscriptions,
|
||||||
type: 'persist',
|
type: 'persist',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -65,7 +64,7 @@ export const getCountdownOptions = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
type CountdownOptions = {
|
type CountdownOptions = {
|
||||||
subscriptions: CountdownSubscription;
|
subscriptions: EntryId[];
|
||||||
secondarySource: keyof OntimeEvent | null;
|
secondarySource: keyof OntimeEvent | null;
|
||||||
showExpected: boolean;
|
showExpected: boolean;
|
||||||
};
|
};
|
||||||
@@ -86,10 +85,8 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
|
|||||||
return searchParams.getAll(key) as EntryId[];
|
return searchParams.getAll(key) as EntryId[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscriptions = getArrayValues('sub');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscriptions: subscriptions.at(0) === 'all' ? 'all' : subscriptions,
|
subscriptions: getArrayValues('sub'),
|
||||||
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
|
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
|
||||||
showExpected: isStringBoolean(getValue('showExpected')),
|
showExpected: isStringBoolean(getValue('showExpected')),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ export function sanitiseTitle(title: string | null) {
|
|||||||
return title ?? '{no title}';
|
return title ?? '{no title}';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CountdownSubscription = EntryId[] | 'all';
|
|
||||||
|
|
||||||
export const preferredFormat12 = 'h:mm a';
|
export const preferredFormat12 = 'h:mm a';
|
||||||
export const preferredFormat24 = 'HH:mm';
|
export const preferredFormat24 = 'HH:mm';
|
||||||
|
|
||||||
@@ -122,7 +120,7 @@ export function useSubscriptionDisplayData(
|
|||||||
/**
|
/**
|
||||||
* Adds a set of subscriptions to the URL parameters
|
* Adds a set of subscriptions to the URL parameters
|
||||||
*/
|
*/
|
||||||
export function makeSubscriptionsUrl(urlRef: string, subscriptions: CountdownSubscription) {
|
export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) {
|
||||||
const url = new URL(urlRef);
|
const url = new URL(urlRef);
|
||||||
const newParams = new URLSearchParams();
|
const newParams = new URLSearchParams();
|
||||||
|
|
||||||
@@ -133,14 +131,10 @@ export function makeSubscriptionsUrl(urlRef: string, subscriptions: CountdownSub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptions === 'all') {
|
// add new subscriptions
|
||||||
newParams.append('sub', 'all');
|
subscriptions.forEach((id) => {
|
||||||
} else {
|
newParams.append('sub', id);
|
||||||
// add new subscriptions
|
});
|
||||||
subscriptions.forEach((id) => {
|
|
||||||
newParams.append('sub', id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
url.search = newParams.toString();
|
url.search = newParams.toString();
|
||||||
|
|
||||||
@@ -152,14 +146,38 @@ export function makeSubscriptionsUrl(urlRef: string, subscriptions: CountdownSub
|
|||||||
* Since the original array is already ordered, we simply filter out the events
|
* Since the original array is already ordered, we simply filter out the events
|
||||||
* which are not in the subscriptions list.
|
* which are not in the subscriptions list.
|
||||||
*/
|
*/
|
||||||
export function getOrderedSubscriptions<T extends OntimeEntry>(
|
export function getOrderedSubscriptions<T extends OntimeEntry>(subscriptions: EntryId[], playableEvents: T[]): T[] {
|
||||||
subscriptions: CountdownSubscription,
|
|
||||||
playableEvents: T[],
|
|
||||||
): T[] {
|
|
||||||
if (subscriptions === 'all') return playableEvents;
|
|
||||||
return playableEvents.filter((event) => subscriptions.includes(event.id));
|
return playableEvents.filter((event) => subscriptions.includes(event.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks through the rundown whether the current event is linked to the loaded event
|
||||||
|
*/
|
||||||
|
export function isLinkedToLoadedEvent(events: OntimeEvent[], loadedId: EntryId | null, currentId: EntryId): boolean {
|
||||||
|
// if nothing is loaded, we return true to simplify the logic
|
||||||
|
if (!loadedId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedIndex = events.findIndex((event) => event.id === loadedId);
|
||||||
|
if (loadedIndex === -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = loadedIndex; i < events.length; i++) {
|
||||||
|
const event = events[i];
|
||||||
|
if (event.id === currentId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.linkStart === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function isOutsideRange(a: number, b: number): boolean {
|
export function isOutsideRange(a: number, b: number): boolean {
|
||||||
return Math.abs(a - b) > MILLIS_PER_MINUTE;
|
return Math.abs(a - b) > MILLIS_PER_MINUTE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { ProjectData } from 'ontime-types';
|
import { parseField } from '../cuesheet.utils';
|
||||||
|
|
||||||
import { makeTable, parseField } from '../cuesheet.utils';
|
|
||||||
|
|
||||||
describe('parseField()', () => {
|
describe('parseField()', () => {
|
||||||
it('returns a string from given millis on timeStart, TimeEnd and duration', () => {
|
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 = {
|
const dragStyle = {
|
||||||
...injectedStyles,
|
...injectedStyles,
|
||||||
zIndex: isDragging ? 2 : 'inherit',
|
zIndex: isDragging ? 2 : 'inherit',
|
||||||
cursor: isDragging ? 'grabbing' : 'grab',
|
|
||||||
transform: CSS.Translate.toString(transform),
|
transform: CSS.Translate.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useVisibleRowsStore } from './visibleRowsStore';
|
|
||||||
|
|
||||||
let observer: IntersectionObserver | null = null;
|
|
||||||
|
|
||||||
function getObserver(): IntersectionObserver {
|
|
||||||
if (!observer) {
|
|
||||||
const options: IntersectionObserverInit = {
|
|
||||||
root: null,
|
|
||||||
rootMargin: '400px 0px', // prevent unmounting rows too early
|
|
||||||
threshold: 0.25,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnIntersect: IntersectionObserverCallback = (entries) => {
|
|
||||||
const visibleRows = useVisibleRowsStore.getState();
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const targetId = entry.target.id;
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
visibleRows.addVisibleRow(targetId);
|
|
||||||
} else {
|
|
||||||
visibleRows.removeVisibleRow(targetId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
observer = new IntersectionObserver(handleOnIntersect, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return observer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* register a row element in the observer
|
|
||||||
*/
|
|
||||||
export function observeRow(element: HTMLElement) {
|
|
||||||
getObserver().observe(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unregister a row element in the observer
|
|
||||||
*/
|
|
||||||
export function unobserveRow(element: HTMLElement) {
|
|
||||||
getObserver().unobserve(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cleanup observer, should be called when the table component unmounts
|
|
||||||
*/
|
|
||||||
export function cleanup() {
|
|
||||||
observer?.disconnect();
|
|
||||||
observer = null;
|
|
||||||
}
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
interface VisibleRowsStore {
|
|
||||||
visibleRows: Set<string>;
|
|
||||||
addVisibleRow: (id: string) => void;
|
|
||||||
removeVisibleRow: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useVisibleRowsStore = create<VisibleRowsStore>((set) => ({
|
|
||||||
visibleRows: new Set(),
|
|
||||||
addVisibleRow: (id) => set((state) => ({ visibleRows: new Set(state.visibleRows).add(id) })),
|
|
||||||
removeVisibleRow: (id) =>
|
|
||||||
set((state) => {
|
|
||||||
const newSet = new Set(state.visibleRows);
|
|
||||||
newSet.delete(id);
|
|
||||||
return { visibleRows: newSet };
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { CustomFields, MaybeNumber, OntimeEntryCommonKeys } from 'ontime-types';
|
||||||
CustomFields,
|
|
||||||
isOntimeDelay,
|
|
||||||
isOntimeEvent,
|
|
||||||
MaybeNumber,
|
|
||||||
OntimeEntry,
|
|
||||||
OntimeEntryCommonKeys,
|
|
||||||
ProjectData,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
type CsvHeaderKey = OntimeEntryCommonKeys | keyof CustomFields;
|
type CsvHeaderKey = OntimeEntryCommonKeys | keyof CustomFields;
|
||||||
@@ -29,72 +21,3 @@ export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
|||||||
|
|
||||||
return String(data ?? '');
|
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 { Playback } from 'ontime-types';
|
||||||
|
|
||||||
import { useIsMobileDevice } from '../../common/hooks/useIsMobileDevice';
|
import { useIsSmallScreen } from '../../common/hooks/useIsSmallScreen';
|
||||||
import { useStudioClockSocket } from '../../common/hooks/useSocket';
|
import { useStudioClockSocket } from '../../common/hooks/useSocket';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../common/utils/time';
|
import { formatTime } from '../../common/utils/time';
|
||||||
@@ -18,12 +18,12 @@ interface StudioClockProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StudioClock({ hideCards }: StudioClockProps) {
|
export default function StudioClock({ hideCards }: StudioClockProps) {
|
||||||
const isMobile = useIsMobileDevice();
|
const isSmallScreen = useIsSmallScreen();
|
||||||
const { clock, playback } = useStudioClockSocket();
|
const { clock, playback } = useStudioClockSocket();
|
||||||
const onAir = playback !== Playback.Stop;
|
const onAir = playback !== Playback.Stop;
|
||||||
|
|
||||||
// if we are on mobile and have to show the cards
|
// if we are on mobile and have to show the cards
|
||||||
if (isMobile && !hideCards) {
|
if (isSmallScreen && !hideCards) {
|
||||||
return <StudioClockMobile clock={clock} onAir={onAir} />;
|
return <StudioClockMobile clock={clock} onAir={onAir} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $view-element-gap;
|
gap: $view-element-gap;
|
||||||
margin-top: 5%;
|
margin-block: auto;
|
||||||
font-size: $base-font-size;
|
font-size: $base-font-size;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ interface StudioTimersProps {
|
|||||||
|
|
||||||
export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const { eventNow, eventNext, message, time, offset, rundown } = useStudioTimersSocket();
|
const { eventNow, eventNext, message, time, offset, rundown, expectedRundownEnd } = useStudioTimersSocket();
|
||||||
|
|
||||||
const schedule = getFormattedScheduleTimes({
|
const schedule = getFormattedScheduleTimes({
|
||||||
offset: offset.absolute,
|
offset: offset,
|
||||||
actualStart: rundown.actualStart,
|
actualStart: rundown.actualStart,
|
||||||
expectedEnd: offset.expectedRundownEnd,
|
expectedEnd: expectedRundownEnd,
|
||||||
});
|
});
|
||||||
const event = getFormattedEventData(eventNow, time);
|
const event = getFormattedEventData(eventNow, time);
|
||||||
const eventNextTitle = eventNext?.title || '-';
|
const eventNextTitle = eventNext?.title || '-';
|
||||||
const formattedTimerMessage = (message.timer.visible && message.timer.text) || '-';
|
const formattedTimerMessage = (message.timer.visible && message.timer.text) || '-';
|
||||||
const formattedSecondaryMessage = message.secondary || '-';
|
const formattedSecondaryMessage = message.timer.secondarySource === 'secondary' ? message.secondary || '-' : '-';
|
||||||
|
|
||||||
// gather presentation styles
|
// gather presentation styles
|
||||||
const timerColour = getTimerColour(
|
const timerColour = getTimerColour(
|
||||||
@@ -37,7 +37,7 @@ export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
|||||||
time.phase === TimerPhase.Danger,
|
time.phase === TimerPhase.Danger,
|
||||||
);
|
);
|
||||||
|
|
||||||
const offsetState = getOffsetState(offset.absolute);
|
const offsetState = getOffsetState(offset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='studio__timers'>
|
<div className='studio__timers'>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
|
|||||||
}
|
}
|
||||||
|
|
||||||
.delay {
|
.delay {
|
||||||
color: $delay-color;
|
color: $ontime-delay-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeOverview {
|
.timeOverview {
|
||||||
@@ -117,7 +117,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
|
|||||||
.cross {
|
.cross {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
text-decoration-color: $delay-color;
|
text-decoration-color: $ontime-delay-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separeLeft {
|
.separeLeft {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { isOntimeEvent, isPlayableEvent, OntimeEntry, PlayableEvent } from 'onti
|
|||||||
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent';
|
import useHorizontalFollowComponent from '../../common/hooks/useHorizontalFollowComponent';
|
||||||
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
|
|
||||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||||
@@ -15,7 +16,7 @@ import style from './Timeline.module.scss';
|
|||||||
|
|
||||||
interface TimelineProps {
|
interface TimelineProps {
|
||||||
firstStart: number;
|
firstStart: number;
|
||||||
rundown: OntimeEntry[];
|
rundown: ExtendedEntry<OntimeEntry>[];
|
||||||
selectedEventId: string | null;
|
selectedEventId: string | null;
|
||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
|
|||||||
|
|
||||||
const { positions, totalWidth } = useMemo(() => {
|
const { positions, totalWidth } = useMemo(() => {
|
||||||
const playableEvents = rundown
|
const playableEvents = rundown
|
||||||
.filter((event): event is PlayableEvent => isOntimeEvent(event) && isPlayableEvent(event))
|
.filter((event): event is ExtendedEntry<PlayableEvent> => isOntimeEvent(event) && isPlayableEvent(event))
|
||||||
.map((event) => ({
|
.map((event) => ({
|
||||||
start: event.timeStart + (event.dayOffset ?? 0) * dayInMs + (event.delay ?? 0),
|
start: event.timeStart + (event.dayOffset ?? 0) * dayInMs + (event.delay ?? 0),
|
||||||
duration: event.duration,
|
duration: event.duration,
|
||||||
@@ -96,7 +97,11 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
|
|||||||
left={position.left}
|
left={position.left}
|
||||||
status={statusMap[event.id]}
|
status={statusMap[event.id]}
|
||||||
start={event.timeStart + (event.dayOffset ?? 0) * dayInMs}
|
start={event.timeStart + (event.dayOffset ?? 0) * dayInMs}
|
||||||
|
totalGap={event.totalGap}
|
||||||
|
isLinkedToLoaded={event.isLinkedToLoaded}
|
||||||
|
dayOffset={event.dayOffset}
|
||||||
title={event.title}
|
title={event.title}
|
||||||
|
cue={event.cue}
|
||||||
width={position.width}
|
width={position.width}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { RefObject } from 'react';
|
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 { getProgress } from '../../common/utils/getProgress';
|
||||||
import { alpha, cx } from '../../common/utils/styleUtils';
|
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 { useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
|
||||||
import { getStatusLabel, getTimeToStart } from './timeline.utils';
|
import { getStatusLabel } from './timeline.utils';
|
||||||
|
|
||||||
import style from './Timeline.module.scss';
|
import style from './Timeline.module.scss';
|
||||||
|
|
||||||
@@ -20,8 +20,12 @@ interface TimelineEntryProps {
|
|||||||
left: number;
|
left: number;
|
||||||
status: ProgressStatus;
|
status: ProgressStatus;
|
||||||
start: number;
|
start: number;
|
||||||
|
dayOffset: number;
|
||||||
|
totalGap: number;
|
||||||
|
isLinkedToLoaded: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
width: number;
|
width: number;
|
||||||
|
cue: string;
|
||||||
ref?: RefObject<HTMLDivElement | null>;
|
ref?: RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +42,12 @@ export function TimelineEntry({
|
|||||||
left,
|
left,
|
||||||
status,
|
status,
|
||||||
start,
|
start,
|
||||||
|
dayOffset,
|
||||||
|
totalGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
title,
|
title,
|
||||||
width,
|
width,
|
||||||
|
cue,
|
||||||
ref,
|
ref,
|
||||||
}: TimelineEntryProps) {
|
}: TimelineEntryProps) {
|
||||||
const formattedStartTime = formatTime(start, formatOptions);
|
const formattedStartTime = formatTime(start, formatOptions);
|
||||||
@@ -61,6 +69,7 @@ export function TimelineEntry({
|
|||||||
left: `${left}px`,
|
left: `${left}px`,
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
}}
|
}}
|
||||||
|
data-testid={cue}
|
||||||
>
|
>
|
||||||
{status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
|
{status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
|
||||||
<div
|
<div
|
||||||
@@ -73,11 +82,29 @@ export function TimelineEntry({
|
|||||||
<div className={style.maybeInline}>
|
<div className={style.maybeInline}>
|
||||||
<div className={cx([hasDelay && style.cross])}>{formattedStartTime}</div>
|
<div className={cx([hasDelay && style.cross])}>{formattedStartTime}</div>
|
||||||
{hasDelay && <div className={style.delay}>{formatTime(delayedStart, formatOptions)}</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>
|
</div>
|
||||||
{showTitle && (
|
{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>
|
<div>{title}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -92,16 +119,31 @@ export function TimelineEntry({
|
|||||||
interface TimelineEntryStatusProps {
|
interface TimelineEntryStatusProps {
|
||||||
delay: number;
|
delay: number;
|
||||||
start: number;
|
start: number;
|
||||||
|
dayOffset: number;
|
||||||
|
totalGap: number;
|
||||||
|
isLinkedToLoaded: boolean;
|
||||||
status: ProgressStatus;
|
status: ProgressStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract component to isolate re-renders provoked by the clock changes
|
// extract component to isolate re-renders provoked by the clock changes
|
||||||
function TimelineEntryStatus({ delay, start, status }: TimelineEntryStatusProps) {
|
function TimelineEntryStatus({
|
||||||
const { clock, offset } = useTimelineStatus();
|
delay,
|
||||||
|
start,
|
||||||
|
dayOffset,
|
||||||
|
totalGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
status,
|
||||||
|
}: TimelineEntryStatusProps) {
|
||||||
|
const state = useExpectedStartData();
|
||||||
|
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
|
|
||||||
// start times need to be normalised in a rundown that crosses midnight
|
const { timeToStart } = getExpectedTimesFromExtendedEvent(
|
||||||
let statusText = getStatusLabel(getTimeToStart(clock, start, delay, offset), status);
|
{ timeStart: start, delay, dayOffset, totalGap, isLinkedToLoaded, countToEnd: false, duration: 0 },
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
let statusText = getStatusLabel(timeToStart, status);
|
||||||
if (statusText === 'live') {
|
if (statusText === 'live') {
|
||||||
statusText = getLocalizedString('timeline.live');
|
statusText = getLocalizedString('timeline.live');
|
||||||
} else if (statusText === 'pending') {
|
} else if (statusText === 'pending') {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
color: $red-500;
|
color: $red-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-content--next {
|
.section-content--followedBy .section-content--next {
|
||||||
color: $green-500;
|
color: $green-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { OntimeEvent } from 'ontime-types';
|
import { OntimeEvent } from 'ontime-types';
|
||||||
|
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||||
|
|
||||||
import { useTimelineSocket } from '../../common/hooks/useSocket';
|
import { useExpectedStartData } from '../../common/hooks/useSocket';
|
||||||
import { formatDuration } from '../../common/utils/time';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
|
import { formatDuration, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
|
||||||
import TimelineSection from './timeline-section/TimelineSection';
|
import TimelineSection from './timeline-section/TimelineSection';
|
||||||
import { getTimeToStart } from './timeline.utils';
|
|
||||||
|
|
||||||
interface TimelineSectionsProps {
|
interface TimelineSectionsProps {
|
||||||
now: OntimeEvent | null;
|
now: ExtendedEntry<OntimeEvent> | null;
|
||||||
next: OntimeEvent | null;
|
next: ExtendedEntry<OntimeEvent> | null;
|
||||||
followedBy: OntimeEvent | null;
|
followedBy: ExtendedEntry<OntimeEvent> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) {
|
export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) {
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const { clock, offset } = useTimelineSocket();
|
const state = useExpectedStartData();
|
||||||
|
|
||||||
// gather card data
|
// gather card data
|
||||||
const titleNow = now?.title ?? '-';
|
const titleNow = now?.title ?? '-';
|
||||||
@@ -26,20 +27,20 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect
|
|||||||
let followedByStatus: string | undefined;
|
let followedByStatus: string | undefined;
|
||||||
|
|
||||||
if (next !== null) {
|
if (next !== null) {
|
||||||
const timeToStart = getTimeToStart(clock, next.timeStart, next?.delay ?? 0, offset);
|
const { timeToStart } = getExpectedTimesFromExtendedEvent(next, state);
|
||||||
if (timeToStart < 0) {
|
if (timeToStart <= 0) {
|
||||||
nextStatus = dueText;
|
nextStatus = dueText;
|
||||||
} else {
|
} else {
|
||||||
nextStatus = `T - ${formatDuration(timeToStart)}`;
|
nextStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (followedBy !== null) {
|
if (followedBy !== null) {
|
||||||
const timeToStart = getTimeToStart(clock, followedBy.timeStart, followedBy?.delay ?? 0, offset);
|
const { timeToStart } = getExpectedTimesFromExtendedEvent(followedBy, state);
|
||||||
if (timeToStart < 0) {
|
if (timeToStart <= 0) {
|
||||||
followedByStatus = dueText;
|
followedByStatus = dueText;
|
||||||
} else {
|
} 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')}
|
title={getLocalizedString('timeline.followedby')}
|
||||||
status={followedByStatus}
|
status={followedByStatus}
|
||||||
content={followedByText}
|
content={followedByText}
|
||||||
category='next'
|
category='followedBy'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MaybeString } from 'ontime-types';
|
|||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
category: 'now' | 'next';
|
category: 'now' | 'next' | 'followedBy';
|
||||||
content: MaybeString;
|
content: MaybeString;
|
||||||
title: string;
|
title: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -16,7 +16,7 @@ function TimelineSection({ category, content, title, status }: SectionProps) {
|
|||||||
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
||||||
const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']);
|
const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']);
|
||||||
return (
|
return (
|
||||||
<div className={sectionClasses}>
|
<div className={sectionClasses} data-testid={category}>
|
||||||
<div className='section-title'>
|
<div className='section-title'>
|
||||||
<span className='section-title__label'>{title}</span>
|
<span className='section-title__label'>{title}</span>
|
||||||
{status && <span className='section-title__status'>{status}</span>}
|
{status && <span className='section-title__status'>{status}</span>}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
getTimeFrom,
|
getTimeFrom,
|
||||||
isNewLatest,
|
isNewLatest,
|
||||||
MILLIS_PER_HOUR,
|
MILLIS_PER_HOUR,
|
||||||
|
MILLIS_PER_MINUTE,
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
|
|
||||||
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { formatDuration } from '../../common/utils/time';
|
import { formatDuration } from '../../common/utils/time';
|
||||||
|
|
||||||
import { useTimelineOptions } from './timeline.options';
|
import { useTimelineOptions } from './timeline.options';
|
||||||
@@ -76,20 +78,23 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeToStart < 0) {
|
if (timeToStart <= 0) {
|
||||||
return 'pending';
|
return 'pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatDuration(timeToStart);
|
return formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScopedRundownData {
|
interface ScopedRundownData {
|
||||||
scopedRundown: PlayableEvent[];
|
scopedRundown: ExtendedEntry<PlayableEvent>[];
|
||||||
firstStart: number;
|
firstStart: number;
|
||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeString): ScopedRundownData {
|
export function useScopedRundown(
|
||||||
|
rundown: ExtendedEntry<OntimeEntry>[],
|
||||||
|
selectedEventId: MaybeString,
|
||||||
|
): ScopedRundownData {
|
||||||
const { hidePast } = useTimelineOptions();
|
const { hidePast } = useTimelineOptions();
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
@@ -97,11 +102,11 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
|
|||||||
return { scopedRundown: [], firstStart: 0, totalDuration: 0 };
|
return { scopedRundown: [], firstStart: 0, totalDuration: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const scopedRundown: PlayableEvent[] = [];
|
const scopedRundown: ExtendedEntry<PlayableEvent>[] = [];
|
||||||
let selectedIndex = selectedEventId ? Infinity : -1;
|
let selectedIndex = selectedEventId ? Infinity : -1;
|
||||||
let firstStart = null;
|
let firstStart = null;
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
let lastEntry: PlayableEvent | null = null;
|
let lastEntry: ExtendedEntry<PlayableEvent> | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < rundown.length; i++) {
|
for (let i = 0; i < rundown.length; i++) {
|
||||||
const currentEntry = rundown[i];
|
const currentEntry = rundown[i];
|
||||||
@@ -150,26 +155,28 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpcomingEvents = {
|
type UpcomingEvents = {
|
||||||
now: OntimeEvent | null;
|
now: ExtendedEntry<OntimeEvent> | null;
|
||||||
next: OntimeEvent | null;
|
next: ExtendedEntry<OntimeEvent> | null;
|
||||||
followedBy: OntimeEvent | null;
|
followedBy: ExtendedEntry<OntimeEvent> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns upcoming events from current: now, next and followedBy
|
* 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) {
|
if (events.length === 0) {
|
||||||
return { now: null, next: null, followedBy: null };
|
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)) {
|
if (!isOntimeEvent(now)) {
|
||||||
now = null;
|
now = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = now ? getNextEvent(events, now.id)?.nextEvent : getFirstEvent(events).firstEvent;
|
const next = now
|
||||||
const followedBy = next ? getNextEvent(events, next.id)?.nextEvent : null;
|
? (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 the titles, handling nulls appropriately
|
||||||
return {
|
return {
|
||||||
@@ -183,7 +190,7 @@ export function getUpcomingEvents(events: PlayableEvent[], selectedId: MaybeStri
|
|||||||
* Utility function calculates time to start
|
* Utility function calculates time to start
|
||||||
*/
|
*/
|
||||||
export function getTimeToStart(now: number, start: number, delay: number, offset: number): number {
|
export function getTimeToStart(now: number, start: number, delay: number, offset: number): number {
|
||||||
return start + delay - now - offset;
|
return start + delay - now + offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimelineLayout {
|
interface TimelineLayout {
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { OntimeEntry, ProjectData, Settings } from 'ontime-types';
|
import { OntimeEntry, ProjectData, Settings } from 'ontime-types';
|
||||||
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
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 useSettings from '../../common/hooks-query/useSettings';
|
||||||
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
||||||
|
|
||||||
export interface TimelineData {
|
export interface TimelineData {
|
||||||
events: OntimeEntry[];
|
events: ExtendedEntry<OntimeEntry>[];
|
||||||
projectData: ProjectData;
|
projectData: ProjectData;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTimelineData(): ViewData<TimelineData> {
|
export function useTimelineData(): ViewData<TimelineData> {
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundown();
|
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../../tsconfig.common.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "esnext",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
@@ -13,21 +14,7 @@
|
|||||||
],
|
],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"allowUnreachableCode": false,
|
|
||||||
"allowUnusedLabels": false,
|
"allowUnusedLabels": false,
|
||||||
"noImplicitThis": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ import svgrPlugin from 'vite-plugin-svgr';
|
|||||||
import { ONTIME_VERSION } from './src/ONTIME_VERSION';
|
import { ONTIME_VERSION } from './src/ONTIME_VERSION';
|
||||||
|
|
||||||
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
|
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
|
||||||
const isDev = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
|
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: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
svgrPlugin(),
|
svgrPlugin(),
|
||||||
!isDev &&
|
sentryAuthToken &&
|
||||||
sentryVitePlugin({
|
sentryVitePlugin({
|
||||||
org: 'get-ontime',
|
org: 'get-ontime',
|
||||||
project: 'ontime',
|
project: 'ontime',
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-electron",
|
"name": "ontime-electron",
|
||||||
"version": "4.0.0-beta.3",
|
"version": "4.0.0-beta.5",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lighdev",
|
|
||||||
"ontime",
|
"ontime",
|
||||||
"timer"
|
"timer",
|
||||||
|
"stage timer",
|
||||||
|
"rundown"
|
||||||
],
|
],
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "37.2.1",
|
"electron": "38.2.1",
|
||||||
"electron-builder": "26.0.18",
|
"electron-builder": "26.0.18",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"eslint-config-prettier": "catalog:",
|
"eslint-config-prettier": "catalog:",
|
||||||
@@ -20,14 +21,12 @@
|
|||||||
"wait-on": "^7.2.0"
|
"wait-on": "^7.2.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "",
|
"dev:electron": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .",
|
||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
"dev": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .",
|
|
||||||
"dist-win": "electron-builder --publish=never --x64 --win",
|
"dist-win": "electron-builder --publish=never --x64 --win",
|
||||||
"dist-mac": "electron-builder --publish=never --mac",
|
"dist-mac": "electron-builder --publish=never --mac",
|
||||||
"dist-mac:local": "electron-builder --publish=never --mac -c.mac.identity=null",
|
"dist-mac:local": "electron-builder --publish=never --mac -c.mac.identity=null",
|
||||||
"dist-linux": "electron-builder --publish=never --x64 --linux",
|
"dist-linux": "electron-builder --publish=never --x64 --linux"
|
||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"productName": "ontime-prerelease",
|
"productName": "ontime-prerelease",
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ function makeSettingsMenu(redirectWindow) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Project data',
|
label: 'Project settings',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Rundown defaults',
|
label: 'Rundown defaults',
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.tgz
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Ontime Resolver
|
||||||
|
|
||||||
|
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
|
||||||
|
|
||||||
|
The Resolver is an attempt to expose our ontime's api so it is easier to integrate with
|
||||||
|
|
||||||
|
## Links
|
||||||
|
- [Ontime's repository](https://github.com/cpvalente/ontime)
|
||||||
|
- [Ontime's documentation](https://docs.getontime.no/)
|
||||||
|
- [Ontime's website](https://getontime.no/)
|
||||||
|
|
||||||
|
## Sponsoring
|
||||||
|
You can help the development of this project or say thank you with a one time donation. \
|
||||||
|
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
|
||||||
|
|
||||||
|
[](https://github.com/sponsors/cpvalente)
|
||||||
|
[](https://www.buymeacoffee.com/cpvalente)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@getontime/resolver",
|
||||||
|
"version": "4.0.0-beta.5",
|
||||||
|
"type": "module",
|
||||||
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
|
"main": "./dist/main.js",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"keywords": ["ontime", "resolver", "parser"],
|
||||||
|
"author": "",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"devDependencies": {
|
||||||
|
"@sprout2000/esbuild-copy-plugin": "^1.1.19",
|
||||||
|
"@typescript-eslint/parser": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"tsup": "^8.5.0",
|
||||||
|
"rimraf": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"ontime-types": "workspace:^4.0.0"
|
||||||
|
},
|
||||||
|
"files": ["dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// api
|
||||||
|
export { MessageTag, RefetchKey } from 'ontime-types';
|
||||||
|
export type { ApiAction, ApiActionTag, ApiResponse } from 'ontime-types';
|
||||||
|
export type { WsPacketToClient, WsPacketToServer } from 'ontime-types';
|
||||||
|
|
||||||
|
// stores
|
||||||
|
export type { RuntimeStore, TimerState, MessageState, RundownState, Offset } from 'ontime-types';
|
||||||
|
export { TimerPhase, Playback, runtimeStorePlaceholder, OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
|
// aux timer
|
||||||
|
export type { SimpleTimerState } from 'ontime-types';
|
||||||
|
export { SimplePlayback, SimpleDirection } from 'ontime-types';
|
||||||
|
|
||||||
|
// entries
|
||||||
|
export type { OntimeEvent, OntimeGroup, EntryCustomFields, CustomFields, Rundown } from 'ontime-types';
|
||||||
|
export { SupportedEntry, isOntimeEvent, isOntimeGroup, isOntimeDelay, isOntimeMilestone } from 'ontime-types';
|
||||||
|
|
||||||
|
// functions
|
||||||
|
export { isWsPacketToClient } from './websocket.js';
|
||||||
|
export type { SocketSender } from './websocket.js';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ApiAction, ApiActionTag, MessageTag, WsPacketToClient, WsPacketToServer } from 'ontime-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper type for sending correct websocket messages to ontime
|
||||||
|
*/
|
||||||
|
export type SocketSender = <T extends MessageTag | ApiActionTag>(
|
||||||
|
tag: T,
|
||||||
|
payload: T extends MessageTag
|
||||||
|
? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload']
|
||||||
|
: Pick<ApiAction & { tag: T }, 'payload'>['payload'],
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a soft type guard for WS packets
|
||||||
|
*/
|
||||||
|
export function isWsPacketToClient(data: unknown): data is WsPacketToClient {
|
||||||
|
return typeof data === 'object' && data !== null && 'tag' in data && 'payload' in data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.common.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "preserve",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": "src",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user