Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6397d33eaa | |||
| 9288566a7f | |||
| 7fd9f83fb0 | |||
| e621f1386e | |||
| b51e7cbd2d | |||
| 7cd92ce5f7 | |||
| f6a02abf36 | |||
| e4b0df42cf | |||
| ded8bddb2d | |||
| 8d3ce46c56 | |||
| 578cb2b244 | |||
| c79ee193c9 | |||
| 5810ae0d54 | |||
| bfbf8574e3 | |||
| 490e429f44 | |||
| 2e950965e0 | |||
| cc34db775a |
@@ -17,6 +17,7 @@
|
||||
# Ignore build folders
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
|
||||
# Ignore default volumes created by running docker compose up
|
||||
ontime-db
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
"ONTIME_VERSION.js"
|
||||
dist/
|
||||
"ONTIME_VERSION.js"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"jest": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "eslint-config-prettier"],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 336 KiB |
@@ -2,7 +2,7 @@ name: Ontime build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['*']
|
||||
tags: [ "*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -11,20 +11,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@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
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -35,7 +30,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- name: Electron - Build app
|
||||
env:
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLEID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
|
||||
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
||||
@@ -58,9 +53,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -85,9 +86,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -103,9 +110,6 @@ jobs:
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
||||
./apps/electron/dist/ontime-linux-arm64.AppImage
|
||||
./apps/electron/dist/ontime-linux-armv7l.AppImage
|
||||
files: './apps/electron/dist/ontime-linux.AppImage'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -14,24 +14,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
|
||||
- uses: actions/setup-node@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- 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
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -43,19 +38,16 @@ jobs:
|
||||
|
||||
- name: Copy server
|
||||
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
|
||||
|
||||
|
||||
- name: Copy client
|
||||
run: cp -R apps/client/build apps/cli/client
|
||||
|
||||
- name: Copy external
|
||||
run: cp -R apps/server/src/external apps/cli/external
|
||||
|
||||
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
|
||||
- name: Install newer version of npm
|
||||
run: npm install -g npm@11
|
||||
|
||||
|
||||
- name: Publish to NPM
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
working-directory: ./apps/cli
|
||||
|
||||
@@ -6,62 +6,43 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
publish_docker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get pnpm store directory
|
||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Build and push stable release
|
||||
if: github.event.release.prerelease == false
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build and push pre-release
|
||||
if: github.event.release.prerelease == true
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:nightly
|
||||
|
||||
- name: Build project packages
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: pnpm build:docker
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Setup Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/ontime
|
||||
tags: |
|
||||
type=semver,pattern=v{{version}}
|
||||
type=semver,pattern=v{{major}}
|
||||
type=raw,value=nightly,enable=${{ github.event.release.prerelease == true }}
|
||||
|
||||
- name: Build and push stable release
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,48 +0,0 @@
|
||||
name: Ontime Resolver build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_resolver:
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- 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
|
||||
|
||||
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
|
||||
- name: Install newer version of npm
|
||||
run: npm install -g npm@11
|
||||
|
||||
- name: Publish to NPM
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
working-directory: ./apps/resolver
|
||||
@@ -14,35 +14,55 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@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
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Run code quality
|
||||
- name: Run linter
|
||||
run: pnpm lint
|
||||
# Run code quality per package
|
||||
- name: React - Run linter + TypeScript checks
|
||||
if: always()
|
||||
run: pnpm lint && tsc --noEmit
|
||||
working-directory: ./apps/client
|
||||
|
||||
- name: Run TypeScript checks
|
||||
run: pnpm typecheck
|
||||
- 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
|
||||
working-directory: ./packages/types
|
||||
|
||||
# We choose to run tests separately
|
||||
- name: Run unit tests
|
||||
- name: React - Run unit tests
|
||||
if: always()
|
||||
run: pnpm test
|
||||
run: pnpm test:pipeline
|
||||
working-directory: ./apps/client
|
||||
|
||||
- name: Server - Run unit tests
|
||||
if: always()
|
||||
run: pnpm test:pipeline
|
||||
working-directory: ./apps/server
|
||||
|
||||
- name: Utils - Run unit tests
|
||||
if: always()
|
||||
run: pnpm test:pipeline
|
||||
working-directory: ./packages/utils
|
||||
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -50,36 +70,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@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
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get installed Playwright version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --parseable | cut -s -d '@' -f3 | cut -d '/' -f1)" >> $GITHUB_ENV
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
- run: npx playwright install --with-deps
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
- name: Build client
|
||||
run: pnpm build:local
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
@@ -40,7 +40,6 @@ override.css
|
||||
|
||||
# working stuff
|
||||
**/TODO.md
|
||||
**.local.**
|
||||
|
||||
# docker utils
|
||||
ontime-db
|
||||
|
||||
@@ -5,10 +5,6 @@ node_modules
|
||||
|
||||
playwright-report
|
||||
|
||||
|
||||
pnpm-lock.yaml
|
||||
**/*.toml
|
||||
**/*.json
|
||||
!tsconfig.common.json
|
||||
!turbo.json
|
||||
!package.json
|
||||
**/*.yml
|
||||
**/*.json
|
||||
@@ -21,8 +21,7 @@ Locally, we would need to run both the React client and the node.js server in de
|
||||
|
||||
From the project root, run the following commands
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __Create a local build__ by running `pnpm build`, this will populate local dependencies
|
||||
- __Run dev mode__ by running `pnpm dev` or `pnpm dev:electron` to get the electron window
|
||||
- __Run dev mode__ by running `pnpm turbo dev`
|
||||
|
||||
|
||||
### Debugging backend
|
||||
@@ -31,10 +30,10 @@ The previous command will start the development servers for both the client, ser
|
||||
Typically in dev mode we prefer to start these in separate terminals to help with error tracking and debugging.
|
||||
|
||||
We do that by creating two terminals an running
|
||||
- __Run the React UI__ by running `pnpm dev --filter=ontime-ui`
|
||||
- __Run the nodejs server__ by running `pnpm dev --filter=ontime-server`
|
||||
- __Run the React UI__ by running `pnpm turbo dev --filter=ontime-ui`
|
||||
- __Run the nodejs server__ by running `pnpm turbo dev --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`.
|
||||
- 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`.
|
||||
|
||||
## TESTING
|
||||
|
||||
@@ -47,7 +46,7 @@ Generally we have 2 types of tests.
|
||||
|
||||
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
||||
|
||||
You can run unit tests by running `pnpm test:pipeline` from the project root.
|
||||
You can run unit tests by running `pnpm turbo test:pipeline` from the project root.
|
||||
This will run all tests and close test runner.
|
||||
|
||||
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
||||
@@ -67,7 +66,7 @@ start the webserver with `pnpm dev:server`
|
||||
|
||||
Some other useful commands
|
||||
|
||||
- `pnpm e2e:ui` open playwright UI
|
||||
- `pnpm e2e --ui` open playwright UI
|
||||
- `pnpm e2e --headed` run tests with a visible browser window
|
||||
|
||||
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
||||
@@ -78,11 +77,14 @@ You can generate a distribution for your OS by running the following steps.
|
||||
From the project root, run the following commands
|
||||
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __Build the UI and server__ by running `pnpm build`
|
||||
- __Create the package__ by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
||||
- __Build the UI and server__ by running `pnpm turbo run build:electron`
|
||||
- __Create the package__ by running `pnpm turbo run dist-win`, `pnpm turbo run dist-mac` or `pnpm turbo run dist-linux`
|
||||
|
||||
The build distribution assets will be at `.apps/electron/dist`
|
||||
|
||||
Note: The MacOS build will only work in CI, locally it will fail due to notarisation issues.
|
||||
Use the `pnpm turbo run dist-mac:local` command to build a MacOS distribution locally and skip the notary process.
|
||||
|
||||
## DOCKER
|
||||
|
||||
Ontime provides a docker-compose file to aid with building and running docker images.
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
ARG NODE_VERSION=22.15.1
|
||||
FROM node:${NODE_VERSION}-alpine
|
||||
|
||||
# Set environment variables
|
||||
# Environment Variable to signal that we are running production
|
||||
ENV NODE_ENV=docker
|
||||
# Ontime Data path
|
||||
ENV ONTIME_DATA=/data/
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app/
|
||||
|
||||
# Prepare UI
|
||||
COPY apps/client/build/ ./client/
|
||||
|
||||
# Prepare Backend
|
||||
COPY apps/server/dist/ ./server/
|
||||
COPY apps/server/src/external/ ./external/
|
||||
COPY apps/server/src/user/ ./user/
|
||||
COPY apps/server/src/html/ ./html/
|
||||
|
||||
# Export default ports
|
||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||
|
||||
CMD ["node", "server/docker.cjs"]
|
||||
|
||||
# Build and run commands
|
||||
# pnpm build:docker
|
||||
# docker buildx build . -t getontime/ontime
|
||||
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
|
||||
FROM node:22-bullseye AS builder
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN npm install -g pnpm@10.11.0
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
||||
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
||||
|
||||
FROM node:22-alpine
|
||||
|
||||
# Set environment variables
|
||||
# Environment Variable to signal that we are running production
|
||||
ENV NODE_ENV=docker
|
||||
# Ontime Data path
|
||||
ENV ONTIME_DATA=/data/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
# Prepare UI
|
||||
COPY --from=builder /app/apps/client/build ./client/
|
||||
|
||||
# Prepare Backend
|
||||
COPY --from=builder /app/apps/server/dist/ ./server/
|
||||
COPY --from=builder /app/apps/server/src/external/ ./external/
|
||||
COPY --from=builder /app/apps/server/src/user/ ./user/
|
||||
COPY --from=builder /app/apps/server/src/html/ ./html/
|
||||
|
||||
# Export default ports
|
||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||
|
||||
CMD ["node", "server/docker.cjs"]
|
||||
|
||||
# Build and run commands
|
||||
# !!! Note that this command needs pre-build versions of the UI and server apps
|
||||
# docker buildx build . -t getontime/ontime
|
||||
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="32"></a>
|
||||
|
||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe">Windows</a>
|
||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">macOS (Apple Silicon)</a>
|
||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">macOS (Intel)</a>
|
||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-x86_64.AppImage">Linux (Intel / AMD 64-bit)</a>
|
||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-arm64.AppImage">Linux (ARM 64-bit, Raspberry Pi 4+)</a>
|
||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-armv7l.AppImage">Linux (ARM 32-bit, older Raspberry Pi)</a>
|
||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">MacOS Arm</a>
|
||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">MacOS Intel</a>
|
||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage">Linux</a>
|
||||
|
||||
... or
|
||||
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
||||
- Install from <a href="https://www.npmjs.com/package/@getontime/cli">NPM</a>
|
||||
- Install from <a href="https://www.npmjs.com/package/ontime">NPM</a>
|
||||
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
||||
|
||||
## Need help?
|
||||
@@ -28,6 +26,8 @@ We do our best to have most topics covered by the documentation. However, if you
|
||||
Let us know!
|
||||
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
|
||||
|
||||
We would also like to include a testimonials section in our ✨new website✨. It would be great to showcase the diversity of users running Ontime.
|
||||
|
||||
# Ontime
|
||||
|
||||
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "4.4.1",
|
||||
"version": "4.0.0-beta.3",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>ontime</title>
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
background-color: #101010 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,47 +1,51 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "4.4.1",
|
||||
"version": "4.0.0-beta.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "1.0.0",
|
||||
"@base-ui-components/react": "1.0.0-beta.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/open-sans": "^5.2.6",
|
||||
"@mantine/hooks": "^8.3.7",
|
||||
"@mantine/hooks": "^8.2.8",
|
||||
"@sentry/react": "^10.2.0",
|
||||
"@table-nav/react": "^0.0.7",
|
||||
"@tanstack/react-query": "^5.85.9",
|
||||
"@tanstack/react-query-devtools": "^5.85.9",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.11.0",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"csv-stringify": "^6.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.2.3",
|
||||
"react": "^19.1.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-icons": "5.5.0",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-router": "^7.11.0",
|
||||
"react-router": "^7.8.2",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"react-virtuoso": "^4.17.0",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"web-vitals": "^5.1.0",
|
||||
"zustand": "^5.0.9"
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||
"postinstall": "pnpm addversion",
|
||||
"dev": "cross-env BROWSER=none vite",
|
||||
"dev:electron": "pnpm dev",
|
||||
"lint": "eslint . --quiet",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "vite build",
|
||||
"build:local": "cross-env NODE_ENV=local vite build",
|
||||
"build:electron": "cross-env NODE_ENV=local vite build",
|
||||
"build:docker": "cross-env VITE_IS_DOCKER=true vite build",
|
||||
"build:localdocker": "cross-env NODE_ENV=local vite build",
|
||||
"lint": "eslint . --quiet",
|
||||
"test": "vitest",
|
||||
"test:pipeline": "vitest run",
|
||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build",
|
||||
"analyse": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -65,7 +69,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@vitejs/plugin-react": "4.5.1",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { Tooltip } from '@base-ui/react/tooltip';
|
||||
import { Tooltip } from '@base-ui-components/react/tooltip';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
// apply global axios config defaults
|
||||
import './common/api/axios.config';
|
||||
|
||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
||||
import { AppContextProvider } from './common/context/AppContext';
|
||||
import { ontimeQueryClient } from './common/queryClient';
|
||||
import { connectSocket } from './common/utils/socket';
|
||||
import KeepAwake from './features/keep-awake/KeepAwake';
|
||||
import { TranslationProvider } from './translation/TranslationProvider';
|
||||
import AppRouter from './AppRouter';
|
||||
import { baseURI } from './externals';
|
||||
@@ -28,7 +24,6 @@ function App() {
|
||||
<ErrorBoundary>
|
||||
<TranslationProvider>
|
||||
<IdentifyOverlay />
|
||||
<KeepAwake />
|
||||
<AppRouter />
|
||||
</TranslationProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -102,7 +102,6 @@ export default function AppRouter() {
|
||||
path='op'
|
||||
element={
|
||||
<ViewLoader>
|
||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
||||
<Operator />
|
||||
</ViewLoader>
|
||||
}
|
||||
@@ -208,13 +207,13 @@ function PresetView() {
|
||||
/**
|
||||
* Locked presets do not allow configuration changes
|
||||
* Whether the user can navigate is determined by the locked param
|
||||
*
|
||||
* We inject the preset to the context value for the view to consume
|
||||
*/
|
||||
const Component = PresetViewMap[preset.target as OntimeViewPresettable];
|
||||
return (
|
||||
<PresetContext value={preset}>
|
||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings />
|
||||
{preset.target !== OntimeView.Cuesheet && (
|
||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings />
|
||||
)}
|
||||
{Component ? <Component /> : <NotFound />}
|
||||
</PresetContext>
|
||||
);
|
||||
|
||||
@@ -4,15 +4,14 @@ import { TranslationObject } from 'ontime-types';
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
|
||||
import { apiEntryUrl, customTranslationsURL, TRANSLATION } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const assetsPath = `${apiEntryUrl}/assets`;
|
||||
|
||||
/**
|
||||
* HTTP request to get css contents
|
||||
*/
|
||||
export async function getCSSContents(options?: RequestOptions): Promise<string> {
|
||||
const res = await axios.get(`${assetsPath}/css`, { signal: options?.signal });
|
||||
export async function getCSSContents(): Promise<string> {
|
||||
const res = await axios.get(`${assetsPath}/css`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -36,8 +35,8 @@ export async function restoreCSSContents(): Promise<string> {
|
||||
/**
|
||||
* HTTP request to get user translation
|
||||
*/
|
||||
export async function getUserTranslation(options?: RequestOptions): Promise<TranslationObject> {
|
||||
const res = await axios.get(customTranslationsURL, { signal: options?.signal });
|
||||
export async function getUserTranslation(): Promise<TranslationObject> {
|
||||
const res = await axios.get(customTranslationsURL);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,14 @@ import type {
|
||||
} from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const automationsPath = `${apiEntryUrl}/automations`;
|
||||
|
||||
/**
|
||||
* HTTP request to get the automations settings
|
||||
*/
|
||||
export async function getAutomationSettings(options?: RequestOptions): Promise<AutomationSettings> {
|
||||
const res = await axios.get(automationsPath, { signal: options?.signal });
|
||||
export async function getAutomationSettings(): Promise<AutomationSettings> {
|
||||
const res = await axios.get(automationsPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
|
||||
axios.defaults.validateStatus = (status) => {
|
||||
return status >= 200 && status < 300;
|
||||
return (status >= 200 && status < 300) || status === 304;
|
||||
};
|
||||
axios.defaults.timeout = axiosConfig.shortTimeout;
|
||||
|
||||
@@ -2,15 +2,14 @@ import axios from 'axios';
|
||||
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||
|
||||
/**
|
||||
* Requests list of known custom fields
|
||||
*/
|
||||
export async function getCustomFields(options?: RequestOptions): Promise<CustomFields> {
|
||||
const res = await axios.get(customFieldsPath, { signal: options?.signal });
|
||||
export async function getCustomFields(): Promise<CustomFields> {
|
||||
const res = await axios.get(customFieldsPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||
|
||||
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
import { createBlob, downloadBlob } from './utils';
|
||||
|
||||
const dbPath = `${apiEntryUrl}/db`;
|
||||
@@ -12,7 +13,7 @@ const dbPath = `${apiEntryUrl}/db`;
|
||||
* HTTP request to the current DB
|
||||
*/
|
||||
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||
return axios.post(`${dbPath}/download`, { filename }, { timeout: axiosConfig.longTimeout });
|
||||
return axios.post(`${dbPath}/download`, { filename });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,27 @@ 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
|
||||
*/
|
||||
@@ -39,7 +61,6 @@ export async function uploadProjectFile(file: File): Promise<MessageResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('project', file);
|
||||
const response = await axios.post(`${dbPath}/upload`, formData, {
|
||||
timeout: axiosConfig.longTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -79,11 +100,8 @@ export async function quickProject(data: QuickStartData): Promise<MessageRespons
|
||||
/**
|
||||
* HTTP request to get the list of available project files
|
||||
*/
|
||||
export async function getProjects(options?: RequestOptions): Promise<ProjectFileListResponse> {
|
||||
const res = await axios.get(`${dbPath}/all`, {
|
||||
signal: options?.signal,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||
const res = await axios.get(`${dbPath}/all`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
||||
import { CustomFields, Rundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
import { downloadBlob } from './utils';
|
||||
|
||||
const excelPath = `${apiEntryUrl}/excel`;
|
||||
|
||||
@@ -13,54 +10,32 @@ const excelPath = `${apiEntryUrl}/excel`;
|
||||
* upload Excel file to server
|
||||
* @return string - file ID op the uploaded file
|
||||
*/
|
||||
export async function upload(file: File, requestOptions?: RequestOptions): Promise<string[]> {
|
||||
export async function upload(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('excel', file);
|
||||
const response = await axios.post(`${excelPath}/upload`, formData, {
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
await axios.post(`${excelPath}/upload`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Worksheet names
|
||||
* @return string[] - array of available worksheets
|
||||
*/
|
||||
export async function getWorksheetNames(): Promise<string[]> {
|
||||
const response: AxiosResponse<string[]> = await axios.get(`${excelPath}/worksheets`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
type PreviewSpreadsheetResponse = {
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
summary: RundownSummary;
|
||||
};
|
||||
export async function importRundownPreview(
|
||||
options: ImportMap,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<PreviewSpreadsheetResponse> {
|
||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
|
||||
`${excelPath}/preview`,
|
||||
{
|
||||
options,
|
||||
},
|
||||
{
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
},
|
||||
);
|
||||
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||
options,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a xlsx representation of the rundown from the server
|
||||
*/
|
||||
export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) {
|
||||
try {
|
||||
const response = await axios.get(`${excelPath}/${rundownId}/export`, {
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
downloadBlob(response.data, `${fileName ?? 'Ontime_rundown'}.xlsx`);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import axios from 'axios';
|
||||
|
||||
import { apiRepoLatest } from '../../externals';
|
||||
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
export type HasUpdate = {
|
||||
url: string;
|
||||
version: string;
|
||||
@@ -12,8 +10,8 @@ export type HasUpdate = {
|
||||
/**
|
||||
* HTTP request to get the latest version and url from github
|
||||
*/
|
||||
export async function getLatestVersion(options?: RequestOptions): Promise<HasUpdate> {
|
||||
const res = await axios.get(apiRepoLatest, { signal: options?.signal });
|
||||
export async function getLatestVersion(): Promise<HasUpdate> {
|
||||
const res = await axios.get(apiRepoLatest);
|
||||
return {
|
||||
url: res.data.html_url as string,
|
||||
version: res.data.tag_name as string,
|
||||
|
||||
@@ -2,19 +2,14 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { ProjectData, ProjectLogoResponse } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
|
||||
const projectPath = `${apiEntryUrl}/project`;
|
||||
|
||||
/**
|
||||
* HTTP request to fetch project data
|
||||
*/
|
||||
export async function getProjectData(options?: RequestOptions): Promise<ProjectData> {
|
||||
const res = await axios.get(projectPath, {
|
||||
signal: options?.signal,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
export async function getProjectData(): Promise<ProjectData> {
|
||||
const res = await axios.get(projectPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -32,7 +27,6 @@ export async function uploadProjectLogo(file: File): Promise<AxiosResponse<Proje
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
const response = await axios.post(`${projectPath}/upload`, formData, {
|
||||
timeout: axiosConfig.longTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
|
||||
@@ -4,15 +4,14 @@ import { OntimeReport } from 'ontime-types';
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
|
||||
import { apiEntryUrl, REPORT } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
export const reportUrl = `${apiEntryUrl}/report`;
|
||||
|
||||
/**
|
||||
* HTTP request to fetch all reports
|
||||
*/
|
||||
export async function fetchReport(options?: RequestOptions): Promise<OntimeReport> {
|
||||
const res = await axios.get(reportUrl, { signal: options?.signal });
|
||||
export async function fetchReport(): Promise<OntimeReport> {
|
||||
const res = await axios.get(reportUrl);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export type RequestOptions = {
|
||||
signal?: AbortSignal;
|
||||
timeout?: number;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Keep a short global timeout for regular API requests, but allow
|
||||
* longer windows for file transfer and heavy import/export operations.
|
||||
*/
|
||||
export const axiosConfig = {
|
||||
shortTimeout: 20 * 1000, // 20 seconds
|
||||
longTimeout: 3 * 60 * 1000, // 3 minutes
|
||||
} as const;
|
||||
@@ -2,9 +2,7 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
type RundownId = string;
|
||||
const rundownPath = `${apiEntryUrl}/rundowns`;
|
||||
|
||||
// #region operations on project rundowns =========================
|
||||
@@ -12,40 +10,24 @@ const rundownPath = `${apiEntryUrl}/rundowns`;
|
||||
/**
|
||||
* HTTP request to fetch a list of existing rundowns
|
||||
*/
|
||||
export async function fetchProjectRundownList(options?: RequestOptions): Promise<ProjectRundownsList> {
|
||||
const res = await axios.get(rundownPath, { signal: options?.signal });
|
||||
export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
||||
const res = await axios.get(rundownPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to fetch all entries in the currently loaded rundown
|
||||
*/
|
||||
export async function fetchCurrentRundown(options?: RequestOptions): Promise<Rundown> {
|
||||
const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal });
|
||||
if (!isValidRundown(res.data)) {
|
||||
throw new Error('Invalid rundown payload');
|
||||
}
|
||||
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||
const res = await axios.get(`${rundownPath}/current`);
|
||||
return res.data;
|
||||
|
||||
function isValidRundown(x: any): x is Rundown {
|
||||
return (
|
||||
x &&
|
||||
typeof x === 'object' &&
|
||||
typeof x.id === 'string' &&
|
||||
Array.isArray(x.order) &&
|
||||
Array.isArray(x.flatOrder) &&
|
||||
x.entries &&
|
||||
typeof x.entries === 'object' &&
|
||||
typeof x.revision === 'number'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to switch the currently loaded rundown
|
||||
*/
|
||||
export async function loadRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/load`);
|
||||
export async function loadRundown(id: string): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||
return axios.post(`${rundownPath}/${id}/load`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,25 +37,11 @@ export async function createRundown(title: string): Promise<AxiosResponse<Projec
|
||||
return axios.post(rundownPath, { title });
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to duplicate an existing rundown
|
||||
*/
|
||||
export async function duplicateRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/duplicate`);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to rename an existing rundown
|
||||
*/
|
||||
export async function renameRundown(rundownId: RundownId, title: string): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||
return axios.patch(`${rundownPath}/${rundownId}`, { title });
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to delete a rundown
|
||||
*/
|
||||
export async function deleteRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||
return axios.delete(`${rundownPath}/${rundownId}`);
|
||||
export async function deleteRundown(id: string): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||
return axios.delete(`${rundownPath}/${id}`);
|
||||
}
|
||||
|
||||
// #endregion operations on project rundowns ======================
|
||||
@@ -83,7 +51,7 @@ export async function deleteRundown(rundownId: RundownId): Promise<AxiosResponse
|
||||
* HTTP request to post new entry
|
||||
*/
|
||||
export async function postAddEntry(
|
||||
rundownId: RundownId,
|
||||
rundownId: string,
|
||||
data: TransientEventPayload,
|
||||
): Promise<AxiosResponse<OntimeEntry>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/entry`, data);
|
||||
@@ -92,10 +60,7 @@ export async function postAddEntry(
|
||||
/**
|
||||
* HTTP request to edit an entry
|
||||
*/
|
||||
export async function putEditEntry(
|
||||
rundownId: RundownId,
|
||||
data: Partial<OntimeEntry>,
|
||||
): Promise<AxiosResponse<OntimeEntry>> {
|
||||
export async function putEditEntry(rundownId: string, data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
||||
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
||||
}
|
||||
|
||||
@@ -107,7 +72,7 @@ export type BatchEditEntry = {
|
||||
/**
|
||||
* HTTP request to edit multiple events
|
||||
*/
|
||||
export async function putBatchEditEvents(rundownId: RundownId, data: BatchEditEntry): Promise<AxiosResponse<Rundown>> {
|
||||
export async function putBatchEditEvents(rundownId: string, data: BatchEditEntry): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.put(`${rundownPath}/${rundownId}/batch`, data);
|
||||
}
|
||||
|
||||
@@ -120,64 +85,56 @@ export type ReorderEntry = {
|
||||
/**
|
||||
* HTTP request to reorder an entry
|
||||
*/
|
||||
export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry): Promise<AxiosResponse<Rundown>> {
|
||||
export async function patchReorderEntry(rundownId: string, data: ReorderEntry): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.patch(`${rundownPath}/${rundownId}/reorder`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to swap two events
|
||||
*/
|
||||
export async function requestEventSwap(
|
||||
rundownId: RundownId,
|
||||
from: EntryId,
|
||||
to: EntryId,
|
||||
): Promise<AxiosResponse<Rundown>> {
|
||||
export async function requestEventSwap(rundownId: string, from: EntryId, to: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to request application of delay
|
||||
*/
|
||||
export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
export async function requestApplyDelay(rundownId: string, delayId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.patch(`${rundownPath}/${rundownId}/applydelay/${delayId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request for cloning an entry
|
||||
*/
|
||||
export async function postCloneEntry(
|
||||
rundownId: RundownId,
|
||||
entryId: EntryId,
|
||||
options?: { before?: EntryId; after?: EntryId },
|
||||
): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
||||
export async function postCloneEntry(rundownId: string, entryId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request for grouping a list of entries into a group
|
||||
*/
|
||||
export async function requestGroupEntries(rundownId: RundownId, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||
export async function requestGroupEntries(rundownId: string, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/group`, { ids: entryIds });
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request for dissolving of a group
|
||||
*/
|
||||
export async function requestUngroup(rundownId: RundownId, groupId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
export async function requestUngroup(rundownId: string, groupId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/ungroup/${groupId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to delete entries of a given rundown
|
||||
*/
|
||||
export async function deleteEntries(rundownId: RundownId, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||
export async function deleteEntries(rundownId: string, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.delete(`${rundownPath}/${rundownId}/entries`, { data: { ids: entryIds } });
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to delete all entries of a given rundown
|
||||
*/
|
||||
export async function requestDeleteAll(rundownId: RundownId): Promise<AxiosResponse<Rundown>> {
|
||||
export async function requestDeleteAll(rundownId: string): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.delete(`${rundownPath}/${rundownId}/all`);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,14 @@ import axios from 'axios';
|
||||
import { GetInfo, LinkOptions } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const sessionPath = `${apiEntryUrl}/session`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve application info
|
||||
*/
|
||||
export async function getInfo(options?: RequestOptions): Promise<GetInfo> {
|
||||
const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal });
|
||||
export async function getInfo(): Promise<GetInfo> {
|
||||
const res = await axios.get(`${sessionPath}/info`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,14 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { Settings } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const settingsPath = `${apiEntryUrl}/settings`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve application settings
|
||||
*/
|
||||
export async function getSettings(options?: RequestOptions): Promise<Settings> {
|
||||
const res = await axios.get(settingsPath, { signal: options?.signal });
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
const res = await axios.get(settingsPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
|
||||
const sheetsPath = `${apiEntryUrl}/sheets`;
|
||||
|
||||
@@ -25,7 +23,6 @@ export const verifyAuthenticationStatus = async (): Promise<{
|
||||
export const requestConnection = async (
|
||||
file: File,
|
||||
sheetId: string,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<{
|
||||
verification_url: string;
|
||||
user_code: string;
|
||||
@@ -34,8 +31,6 @@ export const requestConnection = async (
|
||||
formData.append('client_secret', file);
|
||||
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -58,50 +53,23 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
|
||||
export const previewRundown = async (
|
||||
sheetId: string,
|
||||
options: ImportMap,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<{
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
summary: RundownSummary;
|
||||
}> => {
|
||||
const response = await axios.post(
|
||||
`${sheetsPath}/${sheetId}/read`,
|
||||
{ options },
|
||||
{
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
},
|
||||
);
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise<string[]> => {
|
||||
const response: AxiosResponse<string[]> = await axios.post(
|
||||
`${sheetsPath}/${sheetId}/worksheets`,
|
||||
undefined,
|
||||
{
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
},
|
||||
);
|
||||
export const getWorksheetNames = async (sheetId: string): Promise<string[]> => {
|
||||
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* HTTP request to upload the rundown to a google sheet
|
||||
*/
|
||||
export const uploadRundown = async (
|
||||
sheetId: string,
|
||||
options: ImportMap,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<void> => {
|
||||
const response = await axios.post(
|
||||
`${sheetsPath}/${sheetId}/write`,
|
||||
{ options },
|
||||
{
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
},
|
||||
);
|
||||
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -2,15 +2,14 @@ import axios from 'axios';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve all presets
|
||||
*/
|
||||
export async function getUrlPresets(options?: RequestOptions): Promise<URLPreset[]> {
|
||||
const res = await axios.get(urlPresetsPath, { signal: options?.signal });
|
||||
export async function getUrlPresets(): Promise<URLPreset[]> {
|
||||
const res = await axios.get(urlPresetsPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import axios from 'axios';
|
||||
import type { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||
|
||||
@@ -10,8 +9,8 @@ const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||
* HTTP request to fetch view settings
|
||||
* @returns
|
||||
*/
|
||||
export async function getViewSettings(options?: RequestOptions): Promise<ViewSettings> {
|
||||
const res = await axios.get(viewSettingsPath, { signal: options?.signal });
|
||||
export async function getViewSettings(): Promise<ViewSettings> {
|
||||
const res = await axios.get(viewSettingsPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
.subtle {
|
||||
background: $gray-1050;
|
||||
color: $blue-400;
|
||||
line-height: 1em;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-white {
|
||||
background: $gray-1050;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $gray-1050;
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: $blue-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $blue-600;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $blue-800;
|
||||
border-color: $blue-900;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $viewer-opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.destructive {
|
||||
background: $red-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $red-600;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $red-800;
|
||||
border-color: $red-900;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-destructive {
|
||||
background: $gray-1050;
|
||||
color: $red-400;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted {
|
||||
background: transparent;
|
||||
color: $blue-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted-white {
|
||||
background: transparent;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ghosted-destructive {
|
||||
background: transparent;
|
||||
color: $red-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
@use './buttonVariants' as base;
|
||||
|
||||
@include base.button-variants;
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
@import './BaseButtonStyles.module.scss';
|
||||
|
||||
.baseButton {
|
||||
position: relative;
|
||||
@@ -82,3 +81,4 @@
|
||||
.fluid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@use './buttonVariants' as base;
|
||||
|
||||
@include base.button-variants;
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
@import './BaseButtonStyles.module.scss';
|
||||
|
||||
.baseIconButton {
|
||||
aspect-ratio: 1;
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
@use '@/theme/themeTokens' as *;
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
|
||||
@mixin button-variants {
|
||||
.subtle {
|
||||
background: $gray-1050;
|
||||
color: $blue-400;
|
||||
line-height: 1em;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-white {
|
||||
background: $gray-1050;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $gray-1050;
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: $blue-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $blue-600;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $blue-800;
|
||||
border-color: $blue-900;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $viewer-opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.destructive {
|
||||
background: $red-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $red-600;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $red-800;
|
||||
border-color: $red-900;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-destructive {
|
||||
background: $gray-1050;
|
||||
color: $red-400;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted {
|
||||
background: transparent;
|
||||
color: $blue-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted-white {
|
||||
background: transparent;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted-destructive {
|
||||
background: transparent;
|
||||
color: $red-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
||||
import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
|
||||
|
||||
import style from './Checkbox.module.scss';
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||
import Button from '../buttons/Button';
|
||||
import Dialog from '../dialog/Dialog';
|
||||
import Info from '../info/Info';
|
||||
import Input from '../input/input/Input';
|
||||
import AppLink from '../link/app-link/AppLink';
|
||||
import Modal from '../modal/Modal';
|
||||
import Select from '../select/Select';
|
||||
|
||||
import style from './RedirectClientModal.module.scss';
|
||||
@@ -57,7 +57,7 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
showCloseButton
|
||||
@@ -98,10 +98,7 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
||||
fluid
|
||||
options={viewOptions}
|
||||
defaultValue={viewOptions[0].value}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
setSelected(value);
|
||||
}}
|
||||
onValueChange={(value) => setSelected(value)}
|
||||
disabled={enabledPresets.length === 0}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PropsWithChildren, useRef, useState } from 'react';
|
||||
import { IoCheckmark, IoCopy } from 'react-icons/io5';
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { IoCopy } from 'react-icons/io5';
|
||||
|
||||
import { copyToClipboard } from '../../utils/copyToClipboard';
|
||||
import copyToClipboard from '../../utils/copyToClipboard';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
import Button from '../buttons/Button';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
@@ -25,19 +26,15 @@ export default function CopyTag({
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await copyToClipboard(copyValue);
|
||||
setCopied(true);
|
||||
const handleClick = () => {
|
||||
copyToClipboard(copyValue);
|
||||
setCopied(true);
|
||||
|
||||
// reset copied state
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// ignore errors
|
||||
// reset copied state
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
transform: translateX(-50%);
|
||||
|
||||
padding-inline: 1rem;
|
||||
min-width: min(600px, 90vw);
|
||||
min-width: min(420px, 90vw);
|
||||
|
||||
background-color: $gray-1250;
|
||||
color: $ui-white;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
||||
|
||||
import IconButton from '../buttons/IconButton';
|
||||
|
||||
@@ -12,7 +12,7 @@ interface DialogProps {
|
||||
showCloseButton?: boolean;
|
||||
showBackdrop?: boolean;
|
||||
bodyElements: ReactNode;
|
||||
footerElements?: ReactNode;
|
||||
footerElements: ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,16 +27,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.item {
|
||||
outline: 0;
|
||||
cursor: default;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
padding-inline: 1rem 2rem;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
gap: 0.5rem;
|
||||
line-height: 1em;
|
||||
|
||||
svg {
|
||||
color: $gray-500;
|
||||
@@ -61,42 +61,6 @@
|
||||
border-radius: 3px;
|
||||
background-color: $gray-1000;
|
||||
}
|
||||
|
||||
&[data-type='destructive'] {
|
||||
color: $red-500;
|
||||
svg {
|
||||
color: $red-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item .iconStrong {
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: $gray-400;
|
||||
font-size: calc(1rem - 2px);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
color: $gray-500;
|
||||
font-size: calc(1rem - 4px);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.separator {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
||||
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
||||
|
||||
import style from './DropdownMenu.module.scss';
|
||||
|
||||
type DropdownMenuItemDivider = { type: 'divider' };
|
||||
type DropdownMenuItem = {
|
||||
type: 'item' | 'destructive';
|
||||
type: 'item';
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: IconType;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
shortcut?: string;
|
||||
};
|
||||
|
||||
export type DropdownMenuOption = DropdownMenuItemDivider | DropdownMenuItem;
|
||||
@@ -33,21 +31,9 @@ export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChil
|
||||
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||
}
|
||||
return (
|
||||
<BaseMenu.Item
|
||||
key={index}
|
||||
className={style.item}
|
||||
onClick={item.onClick}
|
||||
disabled={item.disabled}
|
||||
data-type={item.type}
|
||||
>
|
||||
<span className={style.content}>
|
||||
<span className={style.label}>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description && <span className={style.description}>{item.description}</span>}
|
||||
</span>
|
||||
{item.shortcut && <span className={style.shortcut}>{item.shortcut}</span>}
|
||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
</BaseMenu.Item>
|
||||
);
|
||||
})}
|
||||
@@ -83,14 +69,8 @@ export function PositionedDropdownMenu({ items, isOpen, position, onClose }: Pos
|
||||
}
|
||||
return (
|
||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||
<span className={style.content}>
|
||||
<span className={style.label}>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description && <span className={style.description}>{item.description}</span>}
|
||||
</span>
|
||||
{item.shortcut && <span className={style.shortcut}>{item.shortcut}</span>}
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
</BaseMenu.Item>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.arrow {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.corner {
|
||||
transform: rotate(45deg);
|
||||
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
@@ -22,10 +21,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.offsetCorner {
|
||||
right: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
@@ -56,13 +51,6 @@
|
||||
|
||||
&.vertical {
|
||||
width: 1px;
|
||||
height: 0.75em;
|
||||
height: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
border-radius: $panel-border-radius;
|
||||
background-color: $bg-container-l2;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,13 @@
|
||||
import type { HTMLAttributes, JSX, LabelHTMLAttributes, MouseEventHandler } from 'react';
|
||||
import type { HTMLAttributes, LabelHTMLAttributes } from 'react';
|
||||
import { IconBaseProps } from 'react-icons';
|
||||
import { IoArrowUp } from 'react-icons/io5';
|
||||
import { TbPictureInPictureOff } from 'react-icons/tb';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import style from './EditorUtils.module.scss';
|
||||
|
||||
export function CornerExtract({ className, ...elementProps }: IconBaseProps) {
|
||||
return <IoArrowUp className={cx([style.corner, style.arrow, className])} {...elementProps} />;
|
||||
}
|
||||
|
||||
export function CornerPipButton({ className, ...elementProps }: IconBaseProps) {
|
||||
return <TbPictureInPictureOff className={cx([style.corner, style.offsetCorner, className])} {...elementProps} />;
|
||||
}
|
||||
|
||||
interface ExtractAndPip extends IconBaseProps {
|
||||
onExtractClick: MouseEventHandler<SVGElement>;
|
||||
pipElement: JSX.Element;
|
||||
}
|
||||
|
||||
export function CornerWithPip({ className, pipElement, onExtractClick }: ExtractAndPip) {
|
||||
return (
|
||||
<>
|
||||
<IoArrowUp className={cx([style.corner, style.arrow, className])} onClick={onExtractClick} />
|
||||
{/* the pip element returns the icon button */}
|
||||
{pipElement}
|
||||
</>
|
||||
);
|
||||
export function Corner({ className, ...elementProps }: IconBaseProps) {
|
||||
return <IoArrowUp className={cx([style.corner, className])} {...elementProps} />;
|
||||
}
|
||||
|
||||
export function Title({ children, className, ...elementProps }: HTMLAttributes<HTMLHeadingElement>) {
|
||||
@@ -55,7 +35,3 @@ interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
|
||||
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
|
||||
return <div className={cx([style.separator, style[orientation], className])} role='separator' {...elementProps} />;
|
||||
}
|
||||
|
||||
export function Panel({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cx([style.panel, className])} {...props} />;
|
||||
}
|
||||
|
||||
@@ -49,14 +49,15 @@ class ErrorBoundary extends React.Component {
|
||||
return (
|
||||
<div className={style.errorContainer} data-testid='error-container'>
|
||||
<div>
|
||||
<p className={style.error}>: /</p>
|
||||
<p>Something went wrong.</p>
|
||||
<a
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
<div
|
||||
role='button'
|
||||
className={style.report}
|
||||
href={`mailto:mail@getontime.no?subject=Error%20Report&body=${encodeURIComponent(this.reportContent)}`}
|
||||
onClick={() => Sentry.showReportDialog({ eventId: this.state.eventId })}
|
||||
>
|
||||
Report error
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
role='button'
|
||||
className={style.report}
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: $ui-black;
|
||||
color: $ui-white;
|
||||
}
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
|
||||
.error {
|
||||
color: $error-red;
|
||||
}
|
||||
.error {
|
||||
color: $error-red;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.report {
|
||||
color: $blue-500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
.report {
|
||||
text-decoration: underline $error-red;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $ontime-color;
|
||||
.report:hover {
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.report:active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { IoBan } from 'react-icons/io5';
|
||||
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
@@ -11,13 +10,12 @@ interface SwatchProps {
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export default function Swatch({ color, isSelected, onClick }: SwatchProps) {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
onClick?.(color);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
export default function Swatch(props: SwatchProps) {
|
||||
const { color, isSelected, onClick } = props;
|
||||
|
||||
const handleClick = () => {
|
||||
onClick?.(color);
|
||||
};
|
||||
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
||||
|
||||
if (!color) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Radio } from '@base-ui/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
||||
import { Radio } from '@base-ui-components/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
||||
|
||||
import style from './BlockRadio.module.scss';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useEntryActions } from '../../../hooks/useEntryAction';
|
||||
import Input from '../input/Input';
|
||||
|
||||
import BlockRadio from './BlockRadio';
|
||||
|
||||
@@ -13,13 +13,14 @@ interface DelayInputProps {
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
export default function DelayInput(props: DelayInputProps) {
|
||||
const { eventId, duration } = props;
|
||||
const { updateEntry } = useEntryActions();
|
||||
|
||||
const [value, setValue] = useState<string>('');
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// avoid wrong submit on cancel
|
||||
const ignoreChangeRef = useRef(false);
|
||||
let ignoreChange = false;
|
||||
|
||||
// set internal value on duration change
|
||||
useEffect(() => {
|
||||
@@ -34,8 +35,8 @@ export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
||||
* @param {string} newValue string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = (newValue: string) => {
|
||||
if (ignoreChangeRef.current) {
|
||||
ignoreChangeRef.current = false;
|
||||
if (ignoreChange) {
|
||||
ignoreChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
} else if (event.key === 'Escape') {
|
||||
ignoreChangeRef.current = true;
|
||||
ignoreChange = true;
|
||||
setValue(millisToString(duration));
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-600;
|
||||
color: $gray-500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
||||
import { Popover } from '@base-ui/react/popover';
|
||||
import { Popover } from '@base-ui-components/react/popover';
|
||||
|
||||
import PopoverContents from '../../popover/Popover';
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ interface AppLinkProps {
|
||||
* Component used to navigate to an editor link inside the same window
|
||||
* Handles the path to respect Ontime Clouds base URL
|
||||
*/
|
||||
export default function AppLink({ search, children }: PropsWithChildren<AppLinkProps>) {
|
||||
export default function AppLink(props: PropsWithChildren<AppLinkProps>) {
|
||||
const { search, children } = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => navigate({ search });
|
||||
|
||||
@@ -12,7 +12,9 @@ interface ExternalLinkProps {
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
export default function ExternalLink({ href, inline, children }: ExternalLinkProps) {
|
||||
export default function ExternalLink(props: ExternalLinkProps) {
|
||||
const { href, inline, children } = props;
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
openLink(href);
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
transform: translateX(-50%);
|
||||
|
||||
padding-inline: 1rem;
|
||||
min-width: min(880px, 90vw);
|
||||
min-width: min(680px, 90vw);
|
||||
min-height: min(200px, 10vh);
|
||||
max-width: min(1200px, 90vw);
|
||||
max-width: min(900px, 90vw);
|
||||
|
||||
background-color: $gray-1250;
|
||||
color: $ui-white;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
||||
|
||||
import IconButton from '../buttons/IconButton';
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Modal({
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) onClose();
|
||||
}}
|
||||
disablePointerDismissal
|
||||
dismissible={false}
|
||||
>
|
||||
<BaseDialog.Portal>
|
||||
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { memo } from 'react';
|
||||
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { LuCoffee } from 'react-icons/lu';
|
||||
import { useLocation } from 'react-router';
|
||||
import { Dialog } from '@base-ui/react/dialog';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||
|
||||
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
||||
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
||||
import { isLocalhost } from '../../../externals';
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||
import { useClientStore } from '../../stores/clientStore';
|
||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
@@ -30,12 +27,10 @@ export default memo(NavigationMenu);
|
||||
function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||
const id = useClientStore((store) => store.id);
|
||||
const name = useClientStore((store) => store.name);
|
||||
const isSmallScreen = useIsSmallScreen();
|
||||
|
||||
const [isRenameOpen, handlers] = useDisclosure(false);
|
||||
const { fullscreen, toggle } = useFullscreen();
|
||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||
const { keepAwake, toggleKeepAwake } = useKeepAwakeOptions();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
@@ -58,38 +53,25 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={style.body}>
|
||||
{supportsFullscreen && (
|
||||
<NavigationMenuItem active={fullscreen} onClick={toggle}>
|
||||
Toggle Fullscreen
|
||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
||||
</NavigationMenuItem>
|
||||
)}
|
||||
<NavigationMenuItem active={fullscreen} onClick={toggle}>
|
||||
Toggle Fullscreen
|
||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem active={mirror} onClick={() => toggleMirror()}>
|
||||
Flip Screen
|
||||
<IoSwapVertical />
|
||||
{mirror && <span className={style.note}>Active</span>}
|
||||
</NavigationMenuItem>
|
||||
{canUseWakeLock && (
|
||||
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
||||
Keep Awake
|
||||
<LuCoffee />
|
||||
{keepAwake && <span className={style.note}>Active</span>}
|
||||
</NavigationMenuItem>
|
||||
)}
|
||||
<NavigationMenuItem onClick={handlers.open}>Rename Client</NavigationMenuItem>
|
||||
|
||||
<hr className={style.separator} />
|
||||
|
||||
<EditorNavigation />
|
||||
<ClientLink
|
||||
to='cuesheet'
|
||||
current={location.pathname === '/cuesheet'}
|
||||
postAction={isSmallScreen ? onClose : undefined}
|
||||
>
|
||||
<ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}>
|
||||
<IoLockClosedOutline />
|
||||
Cuesheet
|
||||
</ClientLink>
|
||||
<ClientLink to='op' current={location.pathname === '/op'} postAction={isSmallScreen ? onClose : undefined}>
|
||||
<ClientLink to='op' current={location.pathname === '/op'}>
|
||||
<IoLockClosedOutline />
|
||||
Operator
|
||||
</ClientLink>
|
||||
@@ -97,12 +79,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||
<hr className={style.separator} />
|
||||
|
||||
{navigatorConstants.map((route) => (
|
||||
<ClientLink
|
||||
key={route.url}
|
||||
to={route.url}
|
||||
current={location.pathname === `/${route.url}`}
|
||||
postAction={isSmallScreen ? onClose : undefined}
|
||||
>
|
||||
<ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}>
|
||||
{route.label}
|
||||
</ClientLink>
|
||||
))}
|
||||
|
||||
@@ -11,22 +11,15 @@ import style from './ClientLink.module.scss';
|
||||
interface ClientLinkProps {
|
||||
current: boolean;
|
||||
to: string;
|
||||
postAction?: () => void;
|
||||
}
|
||||
|
||||
export default function ClientLink({ current, to, postAction, children }: PropsWithChildren<ClientLinkProps>) {
|
||||
export default function ClientLink({ current, to, children }: PropsWithChildren<ClientLinkProps>) {
|
||||
const { isElectron } = useElectronEvent();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isElectron) {
|
||||
return (
|
||||
<NavigationMenuItem
|
||||
active={current}
|
||||
onClick={() => {
|
||||
handleLinks(to);
|
||||
postAction?.();
|
||||
}}
|
||||
>
|
||||
<NavigationMenuItem active={current} onClick={() => handleLinks(to)}>
|
||||
{children}
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</NavigationMenuItem>
|
||||
@@ -34,13 +27,7 @@ export default function ClientLink({ current, to, postAction, children }: PropsW
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationMenuItem
|
||||
active={current}
|
||||
onClick={() => {
|
||||
navigate(`/${to}`);
|
||||
postAction?.();
|
||||
}}
|
||||
>
|
||||
<NavigationMenuItem active={current} onClick={() => navigate(`/${to}`)}>
|
||||
{children}
|
||||
</NavigationMenuItem>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IoApps, IoSettingsOutline } from 'react-icons/io5';
|
||||
import { IoApps } from 'react-icons/io5';
|
||||
import { IoSettingsOutline } from 'react-icons/io5';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../../hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Popover } from '@base-ui/react/popover';
|
||||
import { Popover } from '@base-ui-components/react/popover';
|
||||
|
||||
import style from './Popover.module.scss';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Radio } from '@base-ui/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
||||
import { Radio } from '@base-ui-components/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
@use '../../../theme/ontimeColours' as *;
|
||||
@use '../../../theme/ontimeStyles' as *;
|
||||
|
||||
// copied from index.scss
|
||||
$track-color: $white-1;
|
||||
$thumb-color: $white-20;
|
||||
$thumb-color-hover: $white-60;
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms;
|
||||
|
||||
&[data-orientation='vertical'] {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&[data-orientation='horizontal'] {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&[data-orientation='horizontal'][data-has-overflow-x] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&[data-scrolling] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
&[data-hovering],
|
||||
&[data-scrolling] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
background: $thumb-color;
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background: $thumb-color-hover;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { CSSProperties, PropsWithChildren, Ref } from 'react';
|
||||
import { ScrollArea } from '@base-ui/react/scroll-area';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import style from './ScrollArea.module.scss';
|
||||
|
||||
interface ScrollAreaProps {
|
||||
className?: string;
|
||||
viewportClassName?: string;
|
||||
contentClassName?: string;
|
||||
contentStyle?: CSSProperties;
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
orientation?: 'vertical' | 'horizontal';
|
||||
}
|
||||
|
||||
export default function StyledScrollArea({
|
||||
className,
|
||||
viewportClassName,
|
||||
contentClassName,
|
||||
contentStyle,
|
||||
children,
|
||||
ref,
|
||||
orientation = 'vertical',
|
||||
}: PropsWithChildren<ScrollAreaProps>) {
|
||||
return (
|
||||
<ScrollArea.Root className={cx([style.root, className])}>
|
||||
<ScrollArea.Viewport ref={ref} className={cx([style.viewport, viewportClassName])}>
|
||||
<ScrollArea.Content className={contentClassName} style={contentStyle}>
|
||||
{children}
|
||||
</ScrollArea.Content>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar className={style.scrollbar} orientation={orientation}>
|
||||
<ScrollArea.Thumb className={style.thumb} />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
);
|
||||
}
|
||||
@@ -62,6 +62,8 @@
|
||||
padding: 2px;
|
||||
border-radius: $component-border-radius-md;
|
||||
color: $ui-white;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
border: 1px solid $gray-1000;
|
||||
|
||||
&[data-side='start'] {
|
||||
@@ -71,16 +73,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
padding-block: 0.25rem;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
max-height: var(--available-height);
|
||||
scroll-padding-block: 1.5rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { LuChevronsUpDown } from 'react-icons/lu';
|
||||
import { Select as BaseSelect } from '@base-ui/react/select';
|
||||
import { Select as BaseSelect } from '@base-ui-components/react/select';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
@@ -31,17 +31,14 @@ export default function Select<T>({ options, fluid, size = 'medium', ...selectRo
|
||||
<BaseSelect.Positioner side='bottom' align='start'>
|
||||
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
||||
<BaseSelect.Popup className={styles.popup}>
|
||||
<BaseSelect.Arrow />
|
||||
<BaseSelect.List className={styles.list}>
|
||||
{options.map(({ disabled, label, value }) => (
|
||||
<BaseSelect.Item key={String(value)} className={styles.item} value={value} disabled={disabled}>
|
||||
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
||||
<IoCheckmark className={styles.itemIndicatorIcon} />
|
||||
</BaseSelect.ItemIndicator>
|
||||
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
|
||||
</BaseSelect.Item>
|
||||
))}
|
||||
</BaseSelect.List>
|
||||
{options.map(({ disabled, label, value }) => (
|
||||
<BaseSelect.Item key={String(value)} className={styles.item} value={value} disabled={disabled}>
|
||||
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
||||
<IoCheckmark className={styles.itemIndicatorIcon} />
|
||||
</BaseSelect.ItemIndicator>
|
||||
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
|
||||
</BaseSelect.Item>
|
||||
))}
|
||||
</BaseSelect.Popup>
|
||||
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
||||
</BaseSelect.Positioner>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Switch as BaseSwitch } from '@base-ui/react/switch';
|
||||
import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import style from './Tag.module.scss';
|
||||
|
||||
interface TagProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Tag({ className, children }: PropsWithChildren<TagProps>) {
|
||||
return <span className={`${style.tag} ${className || ''}`}>{children}</span>;
|
||||
export default function Tag({ children }: { children: ReactNode }) {
|
||||
return <span className={style.tag}>{children}</span>;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
.title-card__title {
|
||||
color: var(--color-override, $viewer-color);
|
||||
padding-right: 1em;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.title-card__placeholder {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
||||
import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
|
||||
|
||||
import style from './Tooltip.module.scss';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { projectLogoPath } from '../../api/constants';
|
||||
|
||||
import './ViewLogo.scss';
|
||||
@@ -9,19 +7,13 @@ interface ViewLogoProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export default function ViewLogo({ name, className }: ViewLogoProps) {
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const hideImage = () => {
|
||||
if (!imageRef.current) return;
|
||||
|
||||
imageRef.current.style.display = 'none';
|
||||
};
|
||||
export default function ViewLogo(props: ViewLogoProps) {
|
||||
const { name, className } = props;
|
||||
|
||||
// we wrap the image in a div to help maintain the aspect ratio
|
||||
return (
|
||||
<div className={className}>
|
||||
<img ref={imageRef} alt='' src={`${projectLogoPath}/${name}`} className='viewLogo' onError={hideImage} />
|
||||
<img alt='' src={`${projectLogoPath}/${name}`} className='viewLogo' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
margin-left: 0.25rem;
|
||||
width: 0.75em;
|
||||
height: 0.75em;
|
||||
background: var(--user-bg, $gray-900);
|
||||
background: var(--user-bg);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: $ui-white;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentProps, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { isStringBoolean } from '../../../views/common/viewUtils';
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
import Checkbox from '../checkbox/Checkbox';
|
||||
import Input from '../input/input/Input';
|
||||
import Select, { SelectOption } from '../select/Select';
|
||||
@@ -160,16 +160,7 @@ function ControlledSelect({ id, initialValue, options }: ControlledSelectProps)
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
size='large'
|
||||
name={id}
|
||||
options={options}
|
||||
value={selected}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
setSelected(value);
|
||||
}}
|
||||
/>
|
||||
<Select size='large' name={id} options={options} value={selected} onValueChange={(value) => setSelected(value)} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
bottom: 0;
|
||||
|
||||
width: 40rem;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { FormEvent, memo } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { Dialog } from '@base-ui/react/dialog';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { OntimeView } from 'ontime-types';
|
||||
|
||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||
import useViewSettings from '../../hooks-query/useViewSettings';
|
||||
import Button from '../buttons/Button';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
import Info from '../info/Info';
|
||||
|
||||
import { ViewOption } from './viewParams.types';
|
||||
import { getPreservedSearchParams, getURLSearchParamsFromObj } from './viewParams.utils';
|
||||
import { getURLSearchParamsFromObj } from './viewParams.utils';
|
||||
import { useViewParamsEditorStore } from './viewParamsEditor.store';
|
||||
import { ViewParamsPresets } from './ViewParamsPresets';
|
||||
import ViewParamsSection from './ViewParamsSection';
|
||||
@@ -25,19 +24,16 @@ interface EditFormDrawerProps {
|
||||
|
||||
export default memo(ViewParamsEditor);
|
||||
function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
const { data: viewSettings } = useViewSettings();
|
||||
const { isOpen, close } = useViewParamsEditorStore();
|
||||
const isSmallScreen = useIsSmallScreen();
|
||||
|
||||
const getPreservedParams = () => getPreservedSearchParams(searchParams, viewOptions);
|
||||
|
||||
const handleClose = () => {
|
||||
close();
|
||||
};
|
||||
|
||||
const resetParams = () => {
|
||||
setSearchParams(getPreservedParams());
|
||||
setSearchParams();
|
||||
};
|
||||
|
||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||
@@ -45,15 +41,7 @@ function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
||||
|
||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, viewOptions);
|
||||
const preservedParams = getPreservedParams();
|
||||
preservedParams.forEach((value, key) => {
|
||||
newSearchParams.append(key, value);
|
||||
});
|
||||
setSearchParams(newSearchParams);
|
||||
|
||||
if (isSmallScreen) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -46,7 +46,7 @@ export function makeCustomFieldSelectOptions(customFields: CustomFields, filterI
|
||||
options.push({
|
||||
value: key,
|
||||
label: value.label,
|
||||
colour: value.colour,
|
||||
colour: value.colour || 'transparent',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,26 +191,3 @@ export function getURLSearchParamsFromObj(paramsObj: ViewParamsObj, paramFields:
|
||||
|
||||
return newSearchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts search params that are not managed by the view params editor
|
||||
* @param currentParams - The current URL search params
|
||||
* @param paramFields - The view options that define the managed parameters
|
||||
* @returns A new URLSearchParams object with preserved params
|
||||
*/
|
||||
export function getPreservedSearchParams(currentParams: URLSearchParams, paramFields: ViewOption[]) {
|
||||
const managedParamIds = new Set<string>();
|
||||
paramFields.forEach((section) => {
|
||||
section.options.forEach((option) => {
|
||||
managedParamIds.add(option.id);
|
||||
});
|
||||
});
|
||||
|
||||
const preserved = new URLSearchParams();
|
||||
currentParams.forEach((value, key) => {
|
||||
if (!managedParamIds.has(key)) {
|
||||
preserved.append(key, value);
|
||||
}
|
||||
});
|
||||
return preserved;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
|
||||
import { useEntryActions } from '../hooks/useEntryAction';
|
||||
|
||||
type EntryActionsContextValue = ReturnType<typeof useEntryActions>;
|
||||
const EntryActionsContext = createContext<EntryActionsContextValue | null>(null);
|
||||
|
||||
interface EntryActionsProviderProps extends PropsWithChildren {
|
||||
actions: EntryActionsContextValue;
|
||||
}
|
||||
|
||||
export function EntryActionsProvider({ children, actions }: EntryActionsProviderProps) {
|
||||
return <EntryActionsContext.Provider value={actions}>{children}</EntryActionsContext.Provider>;
|
||||
}
|
||||
|
||||
export function useEntryActionsContext(): EntryActionsContextValue {
|
||||
const context = useContext(EntryActionsContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useEntryActionsContext must be used within EntryActionsProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export default function useAppVersion() {
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: APP_VERSION,
|
||||
queryFn: ({ signal }) => getLatestVersion({ signal }),
|
||||
queryFn: getLatestVersion,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
|
||||
@@ -8,9 +8,12 @@ import { automationPlaceholderSettings } from '../models/AutomationSettings';
|
||||
export default function useAutomationSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: AUTOMATION,
|
||||
queryFn: ({ signal }) => getAutomationSettings({ signal }),
|
||||
queryFn: getAutomationSettings,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
|
||||
|
||||
@@ -10,9 +10,12 @@ const placeholder: CustomFields = {};
|
||||
export default function useCustomFields() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: CUSTOM_FIELDS,
|
||||
queryFn: ({ signal }) => getCustomFields({ signal }),
|
||||
queryFn: getCustomFields,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? placeholder, status, isFetching, isError, refetch };
|
||||
|
||||
@@ -8,7 +8,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
export function useCustomTranslation() {
|
||||
const { data, status, refetch } = useQuery({
|
||||
queryKey: TRANSLATION,
|
||||
queryFn: ({ signal }) => getUserTranslation({ signal }),
|
||||
queryFn: getUserTranslation,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
@@ -9,9 +9,12 @@ import { ontimePlaceholderInfo } from '../models/Info';
|
||||
export default function useInfo() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
|
||||
queryKey: APP_INFO,
|
||||
queryFn: ({ signal }) => getInfo({ signal }),
|
||||
queryFn: getInfo,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching };
|
||||
|
||||
@@ -8,7 +8,7 @@ import { projectDataPlaceholder } from '../models/ProjectData';
|
||||
export default function useProjectData() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: PROJECT_DATA,
|
||||
queryFn: ({ signal }) => getProjectData({ signal }),
|
||||
queryFn: getProjectData,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ProjectFile, ProjectFileList, ProjectFileListResponse } from 'ontime-types';
|
||||
import { ProjectFileListResponse } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { PROJECT_LIST } from '../api/constants';
|
||||
@@ -14,40 +14,32 @@ const placeholderProjectList: ProjectFileListResponse = {
|
||||
function useProjectList() {
|
||||
const { data, status, refetch } = useQuery({
|
||||
queryKey: PROJECT_LIST,
|
||||
queryFn: ({ signal }) => getProjects({ signal }),
|
||||
queryFn: getProjects,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
return { data: data ?? placeholderProjectList, status, refetch };
|
||||
}
|
||||
|
||||
export type ProjectSortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'modified-asc' | 'modified-desc';
|
||||
type SortComparator = (a: ProjectFile, b: ProjectFile) => number;
|
||||
const sortComparators: Record<ProjectSortMode, SortComparator> = {
|
||||
'alphabetical-asc': (a, b) => a.filename.localeCompare(b.filename),
|
||||
'alphabetical-desc': (a, b) => b.filename.localeCompare(a.filename),
|
||||
'modified-asc': (a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(),
|
||||
'modified-desc': (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
};
|
||||
|
||||
export function useOrderedProjectList(sort: ProjectSortMode = 'modified-desc') {
|
||||
export function useOrderedProjectList() {
|
||||
const response = useProjectList();
|
||||
const { files, lastLoadedProject } = response.data;
|
||||
|
||||
const reorderedProjectFiles: ProjectFileList = useMemo(() => {
|
||||
const reorderedProjectFiles = useMemo(() => {
|
||||
if (!files.length) return [];
|
||||
|
||||
const sorted = [...files].sort(sortComparators[sort]);
|
||||
const currentlyLoadedIndex = files.findIndex((project) => project.filename === lastLoadedProject);
|
||||
|
||||
// keep loaded always on top
|
||||
const currentlyLoadedIndex = sorted.findIndex((project) => project.filename === lastLoadedProject);
|
||||
if (currentlyLoadedIndex > 0) {
|
||||
const [loaded] = sorted.splice(currentlyLoadedIndex, 1);
|
||||
sorted.unshift(loaded);
|
||||
}
|
||||
if (currentlyLoadedIndex === -1) return files;
|
||||
|
||||
return sorted;
|
||||
}, [files, lastLoadedProject, sort]);
|
||||
const projectFiles = [...files];
|
||||
const current = projectFiles.splice(currentlyLoadedIndex, 1)[0];
|
||||
|
||||
return [current, ...projectFiles];
|
||||
}, [files, lastLoadedProject]);
|
||||
|
||||
return { ...response, data: { reorderedProjectFiles, lastLoadedProject: response.data.lastLoadedProject } };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ProjectRundownsList } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { PROJECT_RUNDOWNS } from '../api/constants';
|
||||
import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList, loadRundown, renameRundown } from '../api/rundown';
|
||||
import { createRundown, deleteRundown, fetchProjectRundownList, loadRundown } from '../api/rundown';
|
||||
|
||||
/**
|
||||
* Project rundowns
|
||||
@@ -11,7 +11,7 @@ import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList
|
||||
export function useProjectRundowns() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
|
||||
queryKey: PROJECT_RUNDOWNS,
|
||||
queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
|
||||
queryFn: fetchProjectRundownList,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
@@ -30,26 +30,6 @@ export function useMutateProjectRundowns() {
|
||||
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: duplicate } = useMutation({
|
||||
mutationFn: duplicateRundown,
|
||||
onMutate: () => {
|
||||
ontimeQueryClient.cancelQueries({ queryKey: PROJECT_RUNDOWNS });
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: rename } = useMutation({
|
||||
mutationFn: ([rundownId, title]: Parameters<typeof renameRundown>) => renameRundown(rundownId, title),
|
||||
onMutate: () => {
|
||||
ontimeQueryClient.cancelQueries({ queryKey: PROJECT_RUNDOWNS });
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: remove } = useMutation({
|
||||
mutationFn: deleteRundown,
|
||||
@@ -71,5 +51,5 @@ export function useMutateProjectRundowns() {
|
||||
},
|
||||
});
|
||||
|
||||
return { create, duplicate, remove, load, rename };
|
||||
return { create, remove, load };
|
||||
}
|
||||
|
||||
@@ -8,8 +8,11 @@ import { fetchReport } from '../api/report';
|
||||
export default function useReport() {
|
||||
const { data, refetch } = useQuery<OntimeReport>({
|
||||
queryKey: REPORT,
|
||||
queryFn: ({ signal }) => fetchReport({ signal }),
|
||||
queryFn: fetchReport,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
networkMode: 'always',
|
||||
staleTime: MILLIS_PER_HOUR,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
|
||||
|
||||
@@ -6,7 +6,9 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
import { fetchCurrentRundown } from '../api/rundown';
|
||||
import { useSelectedEventId } from '../hooks/useSocket';
|
||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||
import { getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||
|
||||
import useProjectData from './useProjectData';
|
||||
|
||||
// revision is -1 so that the remote revision is higher
|
||||
const cachedRundownPlaceholder: Rundown = {
|
||||
@@ -24,16 +26,16 @@ const cachedRundownPlaceholder: Rundown = {
|
||||
export default function useRundown() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||
queryKey: RUNDOWN,
|
||||
queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
|
||||
queryFn: fetchCurrentRundown,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
||||
}
|
||||
|
||||
export function useRundownWithMetadata() {
|
||||
const { data, status } = useRundown();
|
||||
const selectedEventId = useSelectedEventId();
|
||||
const { selectedEventId } = useSelectedEventId();
|
||||
const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
||||
return { data, status, rundownMetadata };
|
||||
}
|
||||
@@ -44,20 +46,37 @@ export function useRundownWithMetadata() {
|
||||
*/
|
||||
export function useFlatRundown() {
|
||||
const { data, status } = useRundown();
|
||||
const { data: projectData } = useProjectData();
|
||||
|
||||
const flatRundown = useMemo(() => {
|
||||
if (data.revision === -1) {
|
||||
return [];
|
||||
const loadedProject = useRef<string>('');
|
||||
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
||||
const [flatRundown, setFlatRundown] = useState<OntimeEntry[]>([]);
|
||||
|
||||
// update data whenever the revision changes
|
||||
useEffect(() => {
|
||||
if (data.revision !== -1 && data.revision !== prevRevision) {
|
||||
const flatRundown = data.flatOrder.map((id) => data.entries[id]);
|
||||
setFlatRundown(flatRundown);
|
||||
setPrevRevision(data.revision);
|
||||
}
|
||||
return data.flatOrder.map((id) => data.entries[id]).filter((entry): entry is OntimeEntry => entry !== undefined);
|
||||
}, [data]);
|
||||
}, [data.entries, data.flatOrder, data.revision, prevRevision]);
|
||||
|
||||
// TODO: should we have a project id field?
|
||||
// TODO(v4): cleanup as part of load multiple rundowns
|
||||
// invalidate current version if project changes
|
||||
useEffect(() => {
|
||||
if (projectData?.title !== loadedProject.current) {
|
||||
setPrevRevision(-1);
|
||||
loadedProject.current = projectData?.title ?? '';
|
||||
}
|
||||
}, [projectData]);
|
||||
|
||||
return { data: flatRundown, rundownId: data.id, status };
|
||||
}
|
||||
|
||||
export function useFlatRundownWithMetadata() {
|
||||
const { data, status } = useRundown();
|
||||
const selectedEventId = useSelectedEventId();
|
||||
const { selectedEventId } = useSelectedEventId();
|
||||
|
||||
const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
||||
return { data: rundownWithMetadata, status };
|
||||
@@ -65,13 +84,9 @@ export function useFlatRundownWithMetadata() {
|
||||
|
||||
/**
|
||||
* Provides access to a partial rundown based on a filter callback
|
||||
*
|
||||
* Callers MUST memoize the callback with useCallback to prevent
|
||||
* re-filtering on every render.
|
||||
*
|
||||
*/
|
||||
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
||||
const { data, status } = useFlatRundownWithMetadata();
|
||||
export function usePartialRundown(cb: (event: OntimeEntry) => boolean) {
|
||||
const { data, status } = useFlatRundown();
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter(cb);
|
||||
}, [data, cb]);
|
||||
@@ -85,6 +100,11 @@ export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boo
|
||||
export function useEntry(entryId: EntryId | null): OntimeEntry | null {
|
||||
const { data: rundown } = useRundown();
|
||||
|
||||
if (entryId === null) return null;
|
||||
return rundown.entries[entryId] ?? null;
|
||||
// track the specific entry we care about
|
||||
const entry = useMemo(() => {
|
||||
if (entryId === null) return null;
|
||||
return rundown.entries[entryId];
|
||||
}, [entryId, rundown.entries]);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
||||
export default function useSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: APP_SETTINGS,
|
||||
queryFn: ({ signal }) => getSettings({ signal }),
|
||||
queryFn: getSettings,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
select: (data) => {
|
||||
const unobfuscated = { ...data };
|
||||
|
||||
@@ -13,7 +13,7 @@ interface FetchProps {
|
||||
export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: URL_PRESETS,
|
||||
queryFn: ({ signal }) => getUrlPresets({ signal }),
|
||||
queryFn: getUrlPresets,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
enabled: !skip,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||
export default function useViewSettings() {
|
||||
const { data, status } = useQuery({
|
||||
queryKey: VIEW_SETTINGS,
|
||||
queryFn: ({ signal }) => getViewSettings({ signal }),
|
||||
queryFn: getViewSettings,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
staleTime: MILLIS_PER_HOUR,
|
||||
});
|
||||
|
||||