Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b13d5792f3 | |||
| d0027d4016 | |||
| ae1639802c |
@@ -1,5 +1,5 @@
|
|||||||
# Ignore formatter config files
|
# Ignore config files
|
||||||
.oxfmt*
|
.prettier*
|
||||||
|
|
||||||
# Ignore install stuff
|
# Ignore install stuff
|
||||||
**/yarn.lock
|
**/yarn.lock
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
# Ignore build folders
|
# Ignore build folders
|
||||||
node_modules
|
node_modules
|
||||||
**/node_modules
|
dist
|
||||||
|
|
||||||
# Ignore default volumes created by running docker compose up
|
# Ignore default volumes created by running docker compose up
|
||||||
ontime-db
|
ontime-db
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"ONTIME_VERSION.js"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "eslint-config-prettier"],
|
||||||
|
"plugins": ["@typescript-eslint", "prettier"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"],
|
||||||
|
"extends": ["plugin:playwright/playwright-test"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-useless-concat": "warn",
|
||||||
|
"prefer-template": "warn",
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
"no-console": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allow": ["warn", "error"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prettier/prettier": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
github: cpvalente
|
github: cpvalente
|
||||||
buy_me_a_coffee: cpvalente
|
buy_me_a_coffee: cpvalente
|
||||||
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ['*']
|
tags: [ "*" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -11,20 +11,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v3
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
version: 9
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -35,7 +30,7 @@ jobs:
|
|||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLEID }}
|
APPLE_ID: ${{ secrets.APPLEID }}
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
||||||
@@ -58,9 +53,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -85,9 +86,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -103,9 +110,6 @@ jobs:
|
|||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: './apps/electron/dist/ontime-linux.AppImage'
|
||||||
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
|
||||||
./apps/electron/dist/ontime-linux-arm64.AppImage
|
|
||||||
./apps/electron/dist/ontime-linux-armv7l.AppImage
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -14,24 +14,19 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
|
- name: Set up QEMU
|
||||||
- uses: actions/setup-node@v6
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version: 20
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v3
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
version: 9
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -43,19 +38,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Copy server
|
- name: Copy server
|
||||||
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
|
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
|
||||||
|
|
||||||
- name: Copy client
|
- name: Copy client
|
||||||
run: cp -R apps/client/build apps/cli/client
|
run: cp -R apps/client/build apps/cli/client
|
||||||
|
|
||||||
- name: Copy external
|
- name: Copy external
|
||||||
run: cp -R apps/server/src/external apps/cli/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
|
- name: Publish to NPM
|
||||||
run: pnpm publish --access public --no-git-checks
|
run: pnpm publish --access public --no-git-checks
|
||||||
env:
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
NPM_CONFIG_PROVENANCE: true
|
NPM_CONFIG_PROVENANCE: true
|
||||||
working-directory: ./apps/cli
|
working-directory: ./apps/cli
|
||||||
|
|||||||
@@ -6,62 +6,43 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
publish_docker:
|
publish_docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Docker Login
|
||||||
uses: pnpm/action-setup@v4
|
uses: docker/login-action@v2.1.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Docker Setup Buildx
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Build and push stable release
|
||||||
uses: actions/cache@v4
|
if: github.event.release.prerelease == false
|
||||||
with:
|
uses: docker/build-push-action@v4.0.0
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
with:
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
context: .
|
||||||
restore-keys: |
|
file: ./Dockerfile
|
||||||
${{ runner.os }}-pnpm-store-
|
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
|
- name: Build and push pre-release
|
||||||
run: pnpm install --frozen-lockfile
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v3
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
version: 9
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Run code quality
|
# Run code quality per package
|
||||||
- name: Run linter
|
- name: React - Run linter + TypeScript checks
|
||||||
run: pnpm lint
|
if: always()
|
||||||
|
run: pnpm lint && tsc --noEmit
|
||||||
|
working-directory: ./apps/client
|
||||||
|
|
||||||
- name: Run TypeScript checks
|
- name: Server - Run linter + TypeScript checks
|
||||||
run: pnpm typecheck
|
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
|
# We choose to run tests separately
|
||||||
- name: Run unit tests
|
- name: React - Run unit tests
|
||||||
if: always()
|
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:
|
e2e-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -50,36 +70,21 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v3
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
version: 9
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Get installed Playwright version
|
- name: Build client
|
||||||
run: echo "PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --parseable | cut -s -d '@' -f3 | cut -d '/' -f1)" >> $GITHUB_ENV
|
run: pnpm build:local
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-playwright-
|
|
||||||
- run: npx playwright install --with-deps
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|||||||
@@ -33,15 +33,9 @@ e2e/tests/fixtures/tmp/*
|
|||||||
# production
|
# production
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# bundled assets
|
|
||||||
translations.json
|
|
||||||
override.css
|
|
||||||
|
|
||||||
# working stuff
|
# working stuff
|
||||||
**/TODO.md
|
**/TODO.md
|
||||||
**.local.**
|
|
||||||
|
|
||||||
# docker utils
|
# docker utils
|
||||||
ontime-db
|
ontime-db
|
||||||
@@ -52,4 +46,4 @@ ontime-data/
|
|||||||
**/ONTIME_VERSION.js
|
**/ONTIME_VERSION.js
|
||||||
|
|
||||||
# temporary write files
|
# temporary write files
|
||||||
**.tmp
|
**.tmp
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myCustomPropertiesEnabled" value="true" />
|
||||||
|
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
||||||
|
<option name="myCustomPropertiesList">
|
||||||
|
<value>
|
||||||
|
<list size="3">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="app-region" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredUrls">
|
||||||
|
<list>
|
||||||
|
<option value="http://localhost" />
|
||||||
|
<option value="http://127.0.0.1" />
|
||||||
|
<option value="http://0.0.0.0" />
|
||||||
|
<option value="http://www.w3.org/" />
|
||||||
|
<option value="http://json-schema.org/draft" />
|
||||||
|
<option value="http://java.sun.com/" />
|
||||||
|
<option value="http://xmlns.jcp.org/" />
|
||||||
|
<option value="http://javafx.com/javafx/" />
|
||||||
|
<option value="http://javafx.com/fxml" />
|
||||||
|
<option value="http://maven.apache.org/xsd/" />
|
||||||
|
<option value="http://maven.apache.org/POM/" />
|
||||||
|
<option value="http://www.springframework.org/schema/" />
|
||||||
|
<option value="http://www.springframework.org/tags" />
|
||||||
|
<option value="http://www.springframework.org/security/tags" />
|
||||||
|
<option value="http://www.thymeleaf.org" />
|
||||||
|
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||||
|
<option value="http://www.jboss.com/xml/ns/" />
|
||||||
|
<option value="http://www.ibm.com/webservices/xsd" />
|
||||||
|
<option value="http://activemq.apache.org/schema/" />
|
||||||
|
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||||
|
<option value="http://schemas.xmlsoap.org/" />
|
||||||
|
<option value="http://cxf.apache.org/schemas/" />
|
||||||
|
<option value="http://primefaces.org/ui" />
|
||||||
|
<option value="http://tiles.apache.org/" />
|
||||||
|
<option value="http://__IP__" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120,
|
|
||||||
"sortPackageJson": false,
|
|
||||||
"sortImports": {},
|
|
||||||
"ignorePatterns": [
|
|
||||||
"build",
|
|
||||||
"coverage",
|
|
||||||
"dist",
|
|
||||||
"node_modules",
|
|
||||||
"playwright-report",
|
|
||||||
"pnpm-lock.yaml",
|
|
||||||
"**/*.toml",
|
|
||||||
"**/*.json",
|
|
||||||
"!tsconfig.common.json",
|
|
||||||
"!turbo.json",
|
|
||||||
"!package.json"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
||||||
"categories": {
|
|
||||||
"correctness": "warn",
|
|
||||||
"suspicious": "off",
|
|
||||||
"pedantic": "off",
|
|
||||||
"perf": "warn",
|
|
||||||
"style": "off",
|
|
||||||
"restriction": "off",
|
|
||||||
"nursery": "off"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_",
|
|
||||||
"varsIgnorePattern": "^_",
|
|
||||||
"caughtErrorsIgnorePattern": "^_",
|
|
||||||
"destructuredArrayIgnorePattern": "^_",
|
|
||||||
"caughtErrors": "none"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ignorePatterns": [
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/dist/**",
|
|
||||||
"apps/client/build/**",
|
|
||||||
"apps/electron/assets/**",
|
|
||||||
"**/ONTIME_VERSION.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
build
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
playwright-report
|
||||||
|
|
||||||
|
**/*.toml
|
||||||
|
**/*.yml
|
||||||
|
**/*.json
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
Ontime consists of 3 distinct parts
|
Ontime consists of 3 distinct parts
|
||||||
|
|
||||||
- **client**: A React app for Ontime's UI and web clients
|
- __client__: A React app for Ontime's UI and web clients
|
||||||
- **electron**: An electron app which facilitates the cross-platform distribution of Ontime
|
- __electron__: An electron app which facilitates the cross-platform distribution of Ontime
|
||||||
- **server**: A node application which handles the domains services and integrations
|
- __server__: A node application which handles the domains services and integrations
|
||||||
|
|
||||||
The steps below will assume you have locally installed the necessary dependencies.
|
The steps below will assume you have locally installed the necessary dependencies.
|
||||||
Other dependencies will be installed as part of the setup
|
Other dependencies will be installed as part of the setup
|
||||||
|
|
||||||
- **node** (~22)
|
- __node__ (~20)
|
||||||
- **pnpm** (~10)
|
- __pnpm__ (~9)
|
||||||
- **docker** (only necessary to run and build docker images)
|
- __docker__ (only necessary to run and build docker images)
|
||||||
|
|
||||||
## LOCAL DEVELOPMENT
|
## LOCAL DEVELOPMENT
|
||||||
|
|
||||||
@@ -21,21 +21,18 @@ Locally, we would need to run both the React client and the node.js server in de
|
|||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- **Install the project dependencies** by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- **Create a local build** by running `pnpm build`, this will populate local dependencies
|
- __Run dev mode__ by running `turbo dev`
|
||||||
- **Run dev mode** by running `pnpm dev` or `pnpm dev:electron` to get the electron window
|
|
||||||
|
|
||||||
### Debugging backend
|
### Debugging backend
|
||||||
|
|
||||||
The previous command will start the development servers for both the client, server and electron applications.
|
To debug backend code in Node.js:
|
||||||
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
|
- Open two separate terminals and navigate to the `apps/client` and `apps/server` directories.
|
||||||
|
- In each terminal, run the command `pnpm dev` to start the development servers for both the client and server
|
||||||
- **Run the React UI** by running `pnpm dev --filter=ontime-ui`
|
applications.
|
||||||
- **Run the nodejs server** by running `pnpm dev --filter=ontime-server`
|
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by
|
||||||
|
running `pnpm dev:inspect`.
|
||||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect --filter=ontime-server`.
|
|
||||||
|
|
||||||
## TESTING
|
## TESTING
|
||||||
|
|
||||||
@@ -48,7 +45,7 @@ Generally we have 2 types of tests.
|
|||||||
|
|
||||||
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
||||||
|
|
||||||
You can run unit tests by running `pnpm test:pipeline` from the project root.
|
You can run unit tests by running turbo `turbo test:pipeline` from the project root.
|
||||||
This will run all tests and close test runner.
|
This will run all tests and close test runner.
|
||||||
|
|
||||||
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
||||||
@@ -68,7 +65,7 @@ start the webserver with `pnpm dev:server`
|
|||||||
|
|
||||||
Some other useful commands
|
Some other useful commands
|
||||||
|
|
||||||
- `pnpm e2e:ui` open playwright UI
|
- `pnpm e2e --ui` open playwright UI
|
||||||
- `pnpm e2e --headed` run tests with a visible browser window
|
- `pnpm e2e --headed` run tests with a visible browser window
|
||||||
|
|
||||||
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
||||||
@@ -78,12 +75,15 @@ You can generate a distribution for your OS by running the following steps.
|
|||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- **Install the project dependencies** by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- **Build the UI and server** by running `pnpm build`
|
- __Build the UI and server__ by running `turbo build:electron`
|
||||||
- **Create the package** by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
||||||
|
|
||||||
The build distribution assets will be at `.apps/electron/dist`
|
The build distribution assets will be at `.apps/electron/dist`
|
||||||
|
|
||||||
|
Note: The MacOS build will only work in CI, locally it will fail due to notarisation issues.
|
||||||
|
Use the `turbo dist-mac:local` command to build a MacOS distribution locally.
|
||||||
|
|
||||||
## DOCKER
|
## DOCKER
|
||||||
|
|
||||||
Ontime provides a docker-compose file to aid with building and running docker images.
|
Ontime provides a docker-compose file to aid with building and running docker images.
|
||||||
@@ -91,24 +91,17 @@ While it should allow for a generic setup, it might need to be modified to fit y
|
|||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- **Build docker image from** by running `docker build -t getontime/ontime .`
|
- __Build docker image from__ by running `docker build -t getontime/ontime .`
|
||||||
- **Run docker image from compose** by running `docker-compose up -d`
|
- __Run docker image from compose__ by running `docker-compose up -d`
|
||||||
|
|
||||||
Other useful commands
|
Other useful commands
|
||||||
|
|
||||||
- **List running processes** by running `docker ps`
|
- __List running processes__ by running `docker ps`
|
||||||
- **Kill running process** by running `docker kill <process-id>`
|
- __Kill running process__ by running `docker kill <process-id>`
|
||||||
|
|
||||||
## CONTRIBUTION GUIDELINES
|
## General Info
|
||||||
|
|
||||||
If you want to propose changes to the codebase, please reach out before opening a Pull Request.
|
# APP Building
|
||||||
|
|
||||||
For new PRs, please follow the following checklist:
|
We build the app from app.js for almost all applications. The output file will still be named index.cjs. This is because of Electron.
|
||||||
|
Building the app from index.ts only applies for applications that don't use electron. index.ts will take over the initialization of the server and UI when electron isn't present.
|
||||||
- [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
|
|
||||||
- [ ] You have ran code formatting and linting in all your changes
|
|
||||||
- [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
|
|
||||||
- [ ] The PR body contains description and motivation for the changes
|
|
||||||
|
|
||||||
After this checklist is complete, you can request a review from one of the maintainers to get feedback and approval on the changes. \
|
|
||||||
We will review as soon as possible
|
|
||||||
|
|||||||
@@ -1,30 +1,37 @@
|
|||||||
ARG NODE_VERSION=22.15.1
|
FROM node:20-bullseye AS builder
|
||||||
FROM node:${NODE_VERSION}-alpine
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
# Set environment variables
|
RUN npm install -g pnpm@9.5.0
|
||||||
# Environment Variable to signal that we are running production
|
COPY . /app
|
||||||
ENV NODE_ENV=docker
|
WORKDIR /app
|
||||||
# Ontime Data path
|
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
||||||
ENV ONTIME_DATA=/data/
|
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
||||||
|
|
||||||
RUN mkdir /app
|
FROM node:20-alpine
|
||||||
WORKDIR /app/
|
|
||||||
|
# Set environment variables
|
||||||
# Prepare UI
|
# Environment Variable to signal that we are running production
|
||||||
COPY apps/client/build/ ./client/
|
ENV NODE_ENV=docker
|
||||||
|
# Ontime Data path
|
||||||
# Prepare Backend
|
ENV ONTIME_DATA=/data/
|
||||||
COPY apps/server/dist/ ./server/
|
|
||||||
COPY apps/server/src/external/ ./external/
|
WORKDIR /app/
|
||||||
COPY apps/server/src/user/ ./user/
|
|
||||||
COPY apps/server/src/html/ ./html/
|
# Prepare UI
|
||||||
|
COPY --from=builder /app/apps/client/build ./client/
|
||||||
# Export default ports
|
|
||||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
# Prepare Backend
|
||||||
|
COPY --from=builder /app/apps/server/dist/ ./server/
|
||||||
CMD ["node", "server/docker.cjs"]
|
COPY --from=builder /app/apps/server/src/external/ ./external/
|
||||||
|
COPY --from=builder /app/apps/server/src/user/ ./user/
|
||||||
# Build and run commands
|
COPY --from=builder /app/apps/server/src/html/ ./html/
|
||||||
# pnpm build:docker
|
|
||||||
# docker buildx build . -t getontime/ontime
|
# Export default ports
|
||||||
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
|
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
|
||||||
|
|||||||
@@ -7,31 +7,27 @@
|
|||||||
[](https://www.buymeacoffee.com/cpvalente)
|
[](https://www.buymeacoffee.com/cpvalente)
|
||||||
|
|
||||||
## Download the latest release
|
## Download the latest release
|
||||||
|
|
||||||
<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>
|
<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-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-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 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.AppImage">Linux</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>
|
|
||||||
|
|
||||||
... or
|
... or
|
||||||
|
|
||||||
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
- 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>
|
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
||||||
|
|
||||||
## Need help?
|
## Need help?
|
||||||
|
|
||||||
We do our best to have most topics covered by the documentation. However, if your question is not covered, you are welcome to [fill in a bug report in an issue](https://github.com/cpvalente/ontime/issues), [ask a question in GitHub discussions](https://github.com/cpvalente/ontime/discussions) or hop in the [discord server](https://discord.com/invite/eje3CSUEXm) for a chat.
|
We do our best to have most topics covered by the documentation. However, if your question is not covered, you are welcome to [fill in a bug report in an issue](https://github.com/cpvalente/ontime/issues), [ask a question in GitHub discussions](https://github.com/cpvalente/ontime/discussions) or hop in the [discord server](https://discord.com/invite/eje3CSUEXm) for a chat.
|
||||||
|
|
||||||
## Using Ontime?
|
## Using Ontime?
|
||||||
|
|
||||||
Let us know!
|
Let us know!
|
||||||
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
|
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
|
||||||
|
|
||||||
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
|
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
|
||||||
@@ -47,13 +43,12 @@ Ontime is made by entertainment and broadcast engineers and used by
|
|||||||
- Houses of worship
|
- Houses of worship
|
||||||
|
|
||||||
## Main features
|
## Main features
|
||||||
|
- [x] **Multiplatform**: Available for Windows / MacOS, Linux. You can also self host with the docker image
|
||||||
- **Multiplatform**: Available as a Cloud service and for Windows, macOS, Linux, or self-hosted via Docker.
|
- [x] **In any device**: Ontime is available in the local network to any device with a browser, eg: tablets, mobile phones, laptops, signage, media servers...
|
||||||
- **In any device**: Ontime is available to any device with a browser, eg: tablets, mobile phones, laptops, signage, media servers...
|
- [x] **Made for teams**: Ontime caters to different roles in your production team: directors, operators, backstage and front of house signage...
|
||||||
- **Team Collaboration**: Dedicated views for directors, operators, backstage, and signage.
|
- [x] **Delay workflows**: Manage and communicate runtime delays in real-time to your team
|
||||||
- **Real-Time Updates**: Manage and communicate runtime delays effortlessly.
|
- [x] **Automatable**: Ontime can be fully or partially controlled by an operator, or run standalone with the system clock
|
||||||
- **Automatable**: Ontime can be fully or partially controlled by an operator, or run standalone with the system clock
|
- [x] **Focus on integrations**: Use one of the APIs provided (OSC, HTTP, Websocket) or the available [Companion module](https://bitfocus.io/connections/getontime-ontime) to integrate into your workflow (vMix, disguise, Qlab, OBS)
|
||||||
- **Flexible Integrations**: Use one of the APIs provided (OSC, HTTP, Websocket) or the available [Companion module](https://bitfocus.io/connections/getontime-ontime) to integrate into your workflow (vMix, disguise, Qlab, OBS)
|
|
||||||
|
|
||||||
... and a lot more ...
|
... and a lot more ...
|
||||||
|
|
||||||
@@ -84,29 +79,84 @@ Ontime is made by video engineers and entertainment technicians.
|
|||||||
|
|
||||||
## Using Ontime
|
## Using Ontime
|
||||||
|
|
||||||
### Getting started
|
Ontime can be started by downloading the latest release for your platform. \
|
||||||
|
Alternatively you can also use the docker image, available at [Docker Hub](https://hub.docker.com/r/getontime/ontime)
|
||||||
|
|
||||||
The easiest way to start with Ontime is by leveraging our [Cloud service](https://getontime.no). \
|
Once installed and running, any device in the network has access to Ontime.
|
||||||
This will give you immediate access to running instances of Ontime which are available to share with anyone with an internet connection.
|
|
||||||
|
|
||||||
Alternatively, you can run Ontime locally for free by downloading the latest release for your platform or using the docker image, available at [Docker Hub](https://hub.docker.com/r/getontime/ontime)
|
Ontime provides different screens which allow for different types of interactions with the data. These are called
|
||||||
|
views. \
|
||||||
|
Each view in Ontime focuses on empowering a specific role or achieving a particular task.
|
||||||
|
|
||||||
Once installed and running, any device that shares the same network as Ontime will have access to Ontime.
|
You can access the different views by reaching the ontime server, in your browser, at (_default port
|
||||||
|
4001_) `http://localhost:4001` or `http://192.168.1.3:4001`
|
||||||
|
|
||||||
|
```
|
||||||
|
For the backstage views
|
||||||
|
-------------------------------------------------------------
|
||||||
|
IP.ADDRESS:4001/timer > Presenter / Stage timer view
|
||||||
|
IP.ADDRESS:4001/minimal > Simple timer view
|
||||||
|
IP.ADDRESS:4001/clock > Simple clock view
|
||||||
|
IP.ADDRESS:4001/backstage > Stage Manager / Backstage view
|
||||||
|
IP.ADDRESS:4001/countdown > Countdown to anything
|
||||||
|
IP.ADDRESS:4001/studio > Studio Clock
|
||||||
|
IP.ADDRESS:4001/timeline > Timeline
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
For the public views
|
||||||
|
-------------------------------------------------------------
|
||||||
|
IP.ADDRESS:4001/public > Public / Foyer view
|
||||||
|
IP.ADDRESS:4001/lower > Lower Thirds
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
For production views
|
||||||
|
-------------------------------------------------------------
|
||||||
|
IP.ADDRESS:4001/editor > the control interface, same as the app
|
||||||
|
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
||||||
|
IP.ADDRESS:4001/operator > automated views for operators
|
||||||
|
```
|
||||||
|
|
||||||
More information is available [in our docs](https://docs.getontime.no)
|
More information is available [in our docs](https://docs.getontime.no)
|
||||||
|
|
||||||
## Continued development
|
## Roadmap
|
||||||
|
|
||||||
|
### Continued development
|
||||||
|
|
||||||
Ontime is under active development. We continue adding and improving features in collaboration with users.
|
Ontime is under active development. We continue adding and improving features in collaboration with users.
|
||||||
|
|
||||||
Have an idea? Reach out via [email](mail@getontime.no)
|
Have an idea? Reach out via [email](mail@getontime.no)
|
||||||
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||||
|
|
||||||
## Issues
|
### Issues
|
||||||
|
|
||||||
We use Github's issue tracking for bug reporting and feature requests. \
|
We use Github's issue tracking for bug reporting and feature requests. \
|
||||||
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
||||||
|
|
||||||
|
#### Unsigned App
|
||||||
|
|
||||||
|
When installing the app you would see warning screens from the Operating System like:
|
||||||
|
|
||||||
|
in Windows
|
||||||
|
|
||||||
|
`Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.`
|
||||||
|
|
||||||
|
or in Linux
|
||||||
|
|
||||||
|
`Could Not Display "ontime-linux.AppImage`
|
||||||
|
|
||||||
|
We currently only sign MacOS releases. \
|
||||||
|
Purchasing the certificates for both Mac and Windows would mean a recurrent expense which we are not able to cover.
|
||||||
|
|
||||||
|
You can circumvent this by allowing the execution of the app manually.
|
||||||
|
|
||||||
|
- In Windows: click `more` -> `Run Anyway`
|
||||||
|
- In Linux: right-click the AppImage file: `Properties` -> `Permissions` -> `Allow Executing File as a Program`
|
||||||
|
|
||||||
|
If you have tips on how to improve this or would like to sponsor the code signing,
|
||||||
|
please [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
||||||
@@ -116,10 +166,8 @@ If you are a developer and would like to contribute with code, please open an is
|
|||||||
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
- [Ontime website](https://getontime.no)
|
||||||
- [Ontime website](https://getontime.no)
|
|
||||||
- [Documentation](https://docs.getontime.no)
|
- [Documentation](https://docs.getontime.no)
|
||||||
- [Video tutorials](https://www.youtube.com/@ontimeapp)
|
|
||||||
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
# Terms of sponsorship
|
# Terms of sponsorship
|
||||||
|
|
||||||
Ontime does not exist as a fiscal entity. The sponsorship recipient is the project owner. In this light, contributors are funding the project owner's capacity to work on the project, not the project itself. As a contribution recipient, the code owner reserves the right to redistribute.
|
Ontime does not exist as a fiscal entity. The sponsorship recipient is the project owner. In this light, contributors are funding the project owner's capacity to work on the project, not the project itself. As a contribution recipient, the code owner reserves the right to redistribute.
|
||||||
|
|
||||||
## Why sponsor
|
## Why sponsor
|
||||||
|
|
||||||
Ontime is and will remain free, regardless of your decision to contribute.
|
Ontime is and will remain free, regardless of your decision to contribute.
|
||||||
Regardless, you may choose to provide a monetary contribution:
|
Regardless, you may choose to provide a monetary contribution:
|
||||||
|
|
||||||
### Improve user experience
|
### Improve user experience
|
||||||
|
|
||||||
Certain parts of the application user experience are tied to our capacity to pay for services. App signing and Google Sheets integration are good examples of this.
|
Certain parts of the application user experience are tied to our capacity to pay for services. App signing and Google Sheets integration are good examples of this.
|
||||||
|
|
||||||
### Cool stuff
|
### Cool stuff
|
||||||
|
|
||||||
We may choose not to implement some features. This could be related to the complexity required, to the skill set, or to the fact that we just feel it doesn't improve the app for a significant percentage of users. In those cases, a feature may be considered "not worth it," regardless of its pertinence.
|
We may choose not to implement some features. This could be related to the complexity required, to the skill set, or to the fact that we just feel it doesn't improve the app for a significant percentage of users. In those cases, a feature may be considered "not worth it," regardless of its pertinence.
|
||||||
|
|
||||||
As a user, you can choose to sponsor a particular feature or donate to the app so that we can create bounties for features.
|
As a user, you can choose to sponsor a particular feature or donate to the app so that we can create bounties for features.
|
||||||
|
|
||||||
### Priorities
|
### Priorities
|
||||||
|
|
||||||
Monetary contributions are most immediately needed to offset the costs of presenting and distributing Ontime, i.e., hosting, domains, and certificates.
|
Monetary contributions are most immediately needed to offset the costs of presenting and distributing Ontime, i.e., hosting, domains, and certificates.
|
||||||
|
|
||||||
Additional funding will be considered according to the needs at the time.
|
Additional funding will be considered according to the needs at the time.
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"plugins": [],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-var-requires": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
|
||||||
"extends": ["../../.oxlintrc.json"],
|
|
||||||
"plugins": ["unicorn", "typescript", "oxc", "vitest"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# Ontime CLI
|
# Ontime CLI
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> Ontime is an application for managing rundown and event timers.
|
> Ontime is an application for managing rundown and event timers.
|
||||||
|
|
||||||
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
|
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
|
||||||
@@ -11,40 +10,33 @@ The CLI tool is a fast and lightweight way of installing Ontime and is perfect f
|
|||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
You will need to have installed the correct version of node.js
|
You will need to have installed the correct version of node.js
|
||||||
Please check [the app engines declaration](https://github.com/cpvalente/ontime/blob/master/package.json) for the correct version.
|
Please check [the app engines declaration](https://github.com/cpvalente/ontime/blob/master/package.json) for the correct version.
|
||||||
|
|
||||||
### Running Ontime
|
### Running Ontime
|
||||||
|
|
||||||
To get Ontime running, all you need to do is install it in your system using your package manager of choice:
|
To get Ontime running, all you need to do is install it in your system using your package manager of choice:
|
||||||
|
|
||||||
Install globally in your system
|
Install globally in your system
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @getontime/cli
|
npm install -g @getontime/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
... and run using the installed script
|
... and run using the installed script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ontime
|
ontime
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install and run ontime (the installation here is temporary for the duration of the session)
|
Or install and run ontime (the installation here is temporary for the duration of the session)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx install @getontime/cli
|
npx install @getontime/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [Ontime's repository](https://github.com/cpvalente/ontime)
|
- [Ontime's repository](https://github.com/cpvalente/ontime)
|
||||||
- [Ontime's documentation](https://docs.getontime.no/)
|
- [Ontime's documentation](https://docs.getontime.no/)
|
||||||
- [Ontime's website](https://getontime.no/)
|
- [Ontime's website](https://getontime.no/)
|
||||||
|
|
||||||
## Sponsoring
|
## Sponsoring
|
||||||
|
|
||||||
You can help the development of this project or say thank you with a one time donation. \
|
You can help the development of this project or say thank you with a one time donation. \
|
||||||
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
|
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "4.5.0",
|
"version": "3.13.1",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lighdev",
|
"lighdev",
|
||||||
"ontime",
|
"ontime",
|
||||||
"timer"
|
"timer"
|
||||||
],
|
],
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ontime": "main.js"
|
"ontime": "main.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"client",
|
"client",
|
||||||
"external",
|
"external",
|
||||||
"server"
|
"server"
|
||||||
]
|
],
|
||||||
}
|
"devDependencies": {
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"eslint-config-prettier": "catalog:",
|
||||||
|
"prettier": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:@tanstack/eslint-plugin-query/recommended"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"simple-import-sort",
|
||||||
|
"@tanstack/query"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/jsx-no-bind": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignoreRefs": true,
|
||||||
|
"allowArrowFunctions": true,
|
||||||
|
"allowFunctions": false,
|
||||||
|
"allowBind": false,
|
||||||
|
"ignoreDOMComponents": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-boolean-value": [
|
||||||
|
"error",
|
||||||
|
"never",
|
||||||
|
{
|
||||||
|
"always": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-handler-names": [
|
||||||
|
"off",
|
||||||
|
{
|
||||||
|
"eventHandlerPrefix": "handle",
|
||||||
|
"eventHandlerPropPrefix": "on"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/no-danger": "warn",
|
||||||
|
"react/no-deprecated": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"react/jsx-uses-react": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/jsx-no-undef": "error",
|
||||||
|
"react/jsx-closing-tag-location": "warn",
|
||||||
|
"react/jsx-no-useless-fragment": "warn",
|
||||||
|
"react/no-unescaped-entities": "error",
|
||||||
|
"react/jsx-tag-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"closingSlash": "never",
|
||||||
|
"beforeSelfClosing": "always",
|
||||||
|
"afterOpening": "never",
|
||||||
|
"beforeClosing": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-space-before-closing": [
|
||||||
|
"off",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"react/jsx-curly-brace-presence": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"props": "never",
|
||||||
|
"children": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/destructuring-assignment": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"react/no-unsafe": "off",
|
||||||
|
"react/prop-types": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignore": [],
|
||||||
|
"customValidators": [],
|
||||||
|
"skipUndeclared": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"simple-import-sort/imports": "warn",
|
||||||
|
"simple-import-sort/exports": "warn"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
// override "simple-import-sort" config
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"*.jsx",
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"simple-import-sort/imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
// Packages `react` related packages come first.
|
||||||
|
[
|
||||||
|
"^react",
|
||||||
|
"^@?\\w"
|
||||||
|
],
|
||||||
|
// Internal packages.
|
||||||
|
[
|
||||||
|
"^(@|components)(/.*|$)"
|
||||||
|
],
|
||||||
|
// Side effect imports.
|
||||||
|
[
|
||||||
|
"^\\u0000"
|
||||||
|
],
|
||||||
|
// Parent imports. Put `..` last.
|
||||||
|
[
|
||||||
|
"^\\.\\.(?!/?$)",
|
||||||
|
"^\\.\\./?$"
|
||||||
|
],
|
||||||
|
// Other relative imports. Put same-folder imports and `.` last.
|
||||||
|
[
|
||||||
|
"^\\./(?=.*/)(?!/?$)",
|
||||||
|
"^\\.(?!/?$)",
|
||||||
|
"^\\./?$"
|
||||||
|
],
|
||||||
|
// Style imports.
|
||||||
|
[
|
||||||
|
"^.+\\.?(css)$"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
|
||||||
"extends": ["../../.oxlintrc.json"],
|
|
||||||
"plugins": ["unicorn", "typescript", "oxc", "react", "vitest"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@@ -13,6 +13,12 @@
|
|||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<title>ontime</title>
|
<title>ontime</title>
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
background-color: rgba(0, 0, 0, 0) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@@ -1,47 +1,51 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "4.5.0",
|
"version": "3.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "1.0.0",
|
"@chakra-ui/react": "^2.7.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource/open-sans": "^5.2.6",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@mantine/hooks": "^8.3.7",
|
"@emotion/react": "^11.10.6",
|
||||||
"@sentry/react": "^10.2.0",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@table-nav/react": "^0.0.7",
|
"@fontsource/open-sans": "^5.0.28",
|
||||||
"@tanstack/react-query": "^5.85.9",
|
"@mantine/hooks": "^7.13.3",
|
||||||
"@tanstack/react-query-devtools": "^5.85.9",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@sentry/react": "^8.43.0",
|
||||||
|
"@tanstack/react-query": "^5.62.7",
|
||||||
|
"@tanstack/react-query-devtools": "^5.62.7",
|
||||||
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.2.0",
|
||||||
"csv-stringify": "^6.6.0",
|
"color": "^4.2.3",
|
||||||
"prismjs": "^1.30.0",
|
"csv-stringify": "^6.4.5",
|
||||||
"react": "^19.2.3",
|
"framer-motion": "^10.10.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^18.3.1",
|
||||||
"react-fast-compare": "^3.2.2",
|
"react-fast-compare": "^3.2.2",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.53.1",
|
||||||
"react-icons": "5.5.0",
|
"react-qr-code": "^2.0.12",
|
||||||
"react-qr-code": "^2.0.18",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-router": "^7.11.0",
|
"web-vitals": "^3.1.1",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"zustand": "^5.0.3"
|
||||||
"react-virtuoso": "^4.17.0",
|
|
||||||
"web-vitals": "^5.1.0",
|
|
||||||
"zustand": "^5.0.9"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
"postinstall": "pnpm addversion",
|
"postinstall": "pnpm addversion",
|
||||||
"dev": "cross-env BROWSER=none vite",
|
"dev": "cross-env BROWSER=none vite",
|
||||||
"dev:electron": "pnpm dev",
|
|
||||||
"lint": "oxlint --quiet --type-aware",
|
|
||||||
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
|
||||||
"build": "vite build",
|
"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": "vitest",
|
||||||
"test:pipeline": "vitest run",
|
"test:pipeline": "vitest run",
|
||||||
|
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build",
|
||||||
"analyse": "npx vite-bundle-visualizer"
|
"analyse": "npx vite-bundle-visualizer"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
@@ -58,19 +62,30 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/vite-plugin": "^2.16.1",
|
"@sentry/vite-plugin": "^2.16.1",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@tanstack/eslint-plugin-query": "^5.8.4",
|
||||||
"@types/react": "^19.1.12",
|
"@types/color": "^3.0.3",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react": "^18.0.26",
|
||||||
"@vitejs/plugin-react": "4.5.1",
|
"@types/react-dom": "^18.0.10",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
|
"@typescript-eslint/parser": "catalog:",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"eslint-config-prettier": "catalog:",
|
||||||
|
"eslint-plugin-jest": "^28.6.0",
|
||||||
|
"eslint-plugin-prettier": "catalog:",
|
||||||
|
"eslint-plugin-react": "^7.32.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
|
"happy-dom": "^16.7.2",
|
||||||
"ontime-types": "workspace:*",
|
"ontime-types": "workspace:*",
|
||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
|
"prettier": "catalog:",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "6.3.1",
|
"vite": "^5.2.11",
|
||||||
"vite-plugin-compression2": "2.2.0",
|
"vite-plugin-compression2": "^1.3.3",
|
||||||
"vite-plugin-svgr": "4.3.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "^4.3.1",
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "",
|
"name": "",
|
||||||
"short_name": "",
|
"short_name": "",
|
||||||
"icons": [{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }],
|
"icons": [
|
||||||
|
{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }
|
||||||
|
],
|
||||||
"theme_color": "#2B5ABC",
|
"theme_color": "#2B5ABC",
|
||||||
"background_color": "#101010",
|
"background_color": "#101010",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
|
|||||||
@@ -1,33 +1,30 @@
|
|||||||
import { Tooltip } from '@base-ui/react/tooltip';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { BrowserRouter } from 'react-router';
|
|
||||||
|
|
||||||
// apply global axios config defaults
|
|
||||||
import './common/api/axios.config';
|
|
||||||
import AppRouter from './AppRouter';
|
|
||||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||||
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
import { ontimeQueryClient } from './common/queryClient';
|
import { ontimeQueryClient } from './common/queryClient';
|
||||||
import { connectSocket } from './common/utils/socket';
|
import { connectSocket } from './common/utils/socket';
|
||||||
import { baseURI } from './externals';
|
import theme from './theme/theme';
|
||||||
import KeepAwake from './features/keep-awake/KeepAwake';
|
|
||||||
import { TranslationProvider } from './translation/TranslationProvider';
|
import { TranslationProvider } from './translation/TranslationProvider';
|
||||||
|
import AppRouter from './AppRouter';
|
||||||
|
import { baseURI } from './externals';
|
||||||
|
|
||||||
connectSocket();
|
connectSocket();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={ontimeQueryClient}>
|
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
|
||||||
<AppContextProvider>
|
<QueryClientProvider client={ontimeQueryClient}>
|
||||||
<Tooltip.Provider>
|
<AppContextProvider>
|
||||||
<BrowserRouter basename={baseURI}>
|
<BrowserRouter basename={baseURI}>
|
||||||
<div className='App'>
|
<div className='App'>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<TranslationProvider>
|
<TranslationProvider>
|
||||||
<IdentifyOverlay />
|
<IdentifyOverlay />
|
||||||
<KeepAwake />
|
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@@ -37,9 +34,9 @@ function App() {
|
|||||||
<div id='identify-portal' />
|
<div id='identify-portal' />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Tooltip.Provider>
|
</AppContextProvider>
|
||||||
</AppContextProvider>
|
</QueryClientProvider>
|
||||||
</QueryClientProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,115 +1,168 @@
|
|||||||
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
|
import React from 'react';
|
||||||
import { ComponentType, Suspense, lazy, useEffect, useMemo } from 'react';
|
import {
|
||||||
import { Navigate, Route, useLocation, useNavigate, useParams } from 'react-router';
|
createRoutesFromChildren,
|
||||||
|
matchRoutes,
|
||||||
|
Navigate,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
|
||||||
import ViewNavigationMenu from './common/components/navigation-menu/ViewNavigationMenu';
|
|
||||||
import { PresetContext } from './common/context/PresetContext';
|
|
||||||
import useUrlPresets from './common/hooks-query/useUrlPresets';
|
|
||||||
import { useClientPath } from './common/hooks/useClientPath';
|
import { useClientPath } from './common/hooks/useClientPath';
|
||||||
import { getRouteFromPreset } from './common/utils/urlPresets';
|
|
||||||
import { getIsNavigationLocked, sessionScope } from './externals';
|
|
||||||
import Log from './features/log/Log';
|
import Log from './features/log/Log';
|
||||||
import { initializeSentry } from './sentry.config';
|
import withPreset from './features/PresetWrapper';
|
||||||
import Loader from './views/common/loader/Loader';
|
import withData from './features/viewers/ViewWrapper';
|
||||||
import NotFound from './views/common/not-found/NotFound';
|
|
||||||
import ViewLoader from './views/ViewLoader';
|
import ViewLoader from './views/ViewLoader';
|
||||||
|
import { ONTIME_VERSION } from './ONTIME_VERSION';
|
||||||
|
import { sentryDsn, sentryRecommendedIgnore } from './sentry.config';
|
||||||
|
|
||||||
const Timer = lazy(() => import('./views/timer/Timer'));
|
const Editor = React.lazy(() => import('./features/editors/ProtectedEditor'));
|
||||||
const Countdown = lazy(() => import('./views/countdown/Countdown'));
|
const Cuesheet = React.lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
|
||||||
const Backstage = lazy(() => import('./views/backstage/Backstage'));
|
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
|
||||||
const StudioClock = lazy(() => import('./views/studio/Studio'));
|
|
||||||
const Timeline = lazy(() => import('./views/timeline/TimelinePage'));
|
|
||||||
const ProjectInfo = lazy(() => import('./views/project-info/ProjectInfo'));
|
|
||||||
|
|
||||||
const Editor = lazy(() => import('./views/editor/ProtectedEditor'));
|
const TimerView = React.lazy(() => import('./views/timer/Timer'));
|
||||||
const Cuesheet = lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
|
const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
|
||||||
const Operator = lazy(() => import('./features/operator/OperatorExport'));
|
const ClockView = React.lazy(() => import('./features/viewers/clock/Clock'));
|
||||||
|
const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdown'));
|
||||||
|
|
||||||
const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'));
|
const Backstage = React.lazy(() => import('./views/backstage/Backstage'));
|
||||||
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
const Timeline = React.lazy(() => import('./views/timeline/TimelinePage'));
|
||||||
const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport'));
|
const Public = React.lazy(() => import('./views/public/Public'));
|
||||||
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
|
const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
|
||||||
|
const StudioClock = React.lazy(() => import('./features/viewers/studio/StudioClock'));
|
||||||
|
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
|
||||||
|
|
||||||
// Initialize Sentry with our configuration
|
const STimer = withPreset(withData(TimerView));
|
||||||
const SentryRouter = initializeSentry();
|
const SMinimalTimer = withPreset(withData(MinimalTimerView));
|
||||||
|
const SClock = withPreset(withData(ClockView));
|
||||||
|
const SCountdown = withPreset(withData(Countdown));
|
||||||
|
const SBackstage = withPreset(withData(Backstage));
|
||||||
|
const SProjectInfo = withPreset(withData(ProjectInfo));
|
||||||
|
const SPublic = withPreset(withData(Public));
|
||||||
|
const SLowerThird = withPreset(withData(Lower));
|
||||||
|
const SStudio = withPreset(withData(StudioClock));
|
||||||
|
const STimeline = withPreset(withData(Timeline));
|
||||||
|
const PCuesheet = withPreset(Cuesheet);
|
||||||
|
const POperator = withPreset(Operator);
|
||||||
|
|
||||||
|
const EditorFeatureWrapper = React.lazy(() => import('./features/EditorFeatureWrapper'));
|
||||||
|
const RundownPanel = React.lazy(() => import('./features/rundown/RundownExport'));
|
||||||
|
const TimerControl = React.lazy(() => import('./features/control/playback/TimerControlExport'));
|
||||||
|
const MessageControl = React.lazy(() => import('./features/control/message/MessageControlExport'));
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: sentryDsn,
|
||||||
|
integrations: [
|
||||||
|
Sentry.reactRouterV6BrowserTracingIntegration({
|
||||||
|
useEffect: React.useEffect,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
matchRoutes,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
tracesSampleRate: 0.3,
|
||||||
|
release: ONTIME_VERSION,
|
||||||
|
enabled: import.meta.env.PROD,
|
||||||
|
ignoreErrors: [...sentryRecommendedIgnore, /Unable to preload CSS/i, /dynamically imported module/i],
|
||||||
|
denyUrls: [/extensions\//i, /^chrome:\/\//i, /^chrome-extension:\/\//i],
|
||||||
|
});
|
||||||
|
|
||||||
|
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
||||||
|
|
||||||
export default function AppRouter() {
|
export default function AppRouter() {
|
||||||
// handle client path changes
|
// handle client path changes
|
||||||
useClientPath();
|
useClientPath();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loader />}>
|
<React.Suspense fallback={null}>
|
||||||
<SentryRouter>
|
<SentryRoutes>
|
||||||
<Route path='/' element={<Navigate to='/timer' />} />
|
<Route path='/' element={<Navigate to='/timer' />} />
|
||||||
<Route
|
<Route
|
||||||
path='timer'
|
path='/timer'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
<STimer />
|
||||||
<Timer />
|
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='countdown'
|
path='/public'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
<SPublic />
|
||||||
<Countdown />
|
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='backstage'
|
path='/minimal'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
<SMinimalTimer />
|
||||||
<Backstage />
|
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='studio'
|
path='/clock'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
<SClock />
|
||||||
<StudioClock />
|
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='timeline'
|
path='/countdown'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
<SCountdown />
|
||||||
<Timeline />
|
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='info'
|
path='/backstage'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />
|
<SBackstage />
|
||||||
<ProjectInfo />
|
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/studio'
|
||||||
|
element={
|
||||||
|
<ViewLoader>
|
||||||
|
<SStudio />
|
||||||
|
</ViewLoader>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/*/!* Lower third cannot have a loading screen *!/*/}
|
||||||
|
<Route path='/lower' element={<SLowerThird />} />
|
||||||
|
<Route
|
||||||
|
path='/timeline'
|
||||||
|
element={
|
||||||
|
<ViewLoader>
|
||||||
|
<STimeline />
|
||||||
|
</ViewLoader>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='/info'
|
||||||
|
element={
|
||||||
|
<ViewLoader>
|
||||||
|
<SProjectInfo />
|
||||||
|
</ViewLoader>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*/!* Protected Routes *!/*/}
|
{/*/!* Protected Routes *!/*/}
|
||||||
<Route path='editor' element={<Editor />} />
|
<Route path='/editor' element={<Editor />} />
|
||||||
<Route path='cuesheet' element={<Cuesheet />} />
|
<Route path='/cuesheet' element={<PCuesheet />} />
|
||||||
<Route
|
<Route path='/op' element={<POperator />} />
|
||||||
path='op'
|
|
||||||
element={
|
|
||||||
<ViewLoader>
|
|
||||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
|
||||||
<Operator />
|
|
||||||
</ViewLoader>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/*/!* Protected Routes - Elements *!/*/}
|
{/*/!* Protected Routes - Elements *!/*/}
|
||||||
<Route
|
<Route
|
||||||
path='rundown'
|
path='/rundown'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<RundownPanel />
|
<RundownPanel />
|
||||||
@@ -117,7 +170,7 @@ export default function AppRouter() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='timercontrol'
|
path='/timercontrol'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<TimerControl />
|
<TimerControl />
|
||||||
@@ -125,7 +178,7 @@ export default function AppRouter() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='messagecontrol'
|
path='/messagecontrol'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<MessageControl />
|
<MessageControl />
|
||||||
@@ -133,119 +186,16 @@ export default function AppRouter() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='log'
|
path='/log'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<Log />
|
<Log />
|
||||||
</EditorFeatureWrapper>
|
</EditorFeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/**
|
{/*/!* Send to default if nothing found *!/*/}
|
||||||
* If the views are prefixed with the "preset" path, we are in a locked preset
|
<Route path='*' element={<STimer />} />
|
||||||
* Locked presets do not expose their parameters
|
</SentryRoutes>
|
||||||
*/}
|
</React.Suspense>
|
||||||
<Route path='preset/:alias' element={<PresetView />} />
|
|
||||||
|
|
||||||
{/**
|
|
||||||
* If we havent matched any views or presets, we may be in an unlocked preset
|
|
||||||
* Unlocked presets are unwrapped to expose their target and parameters
|
|
||||||
*/}
|
|
||||||
<Route path='*' element={<RedirectPreset />} />
|
|
||||||
</SentryRouter>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PresetViewMap: Record<OntimeViewPresettable, ComponentType> = {
|
|
||||||
[OntimeView.Cuesheet]: Cuesheet,
|
|
||||||
[OntimeView.Operator]: Operator,
|
|
||||||
[OntimeView.Timer]: Timer,
|
|
||||||
[OntimeView.Backstage]: Backstage,
|
|
||||||
[OntimeView.Timeline]: Timeline,
|
|
||||||
[OntimeView.StudioClock]: StudioClock,
|
|
||||||
[OntimeView.Countdown]: Countdown,
|
|
||||||
[OntimeView.ProjectInfo]: ProjectInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This view will mask a configured canonical route
|
|
||||||
* and inject the preset search parameters to context
|
|
||||||
* User are not able to configure the parameters locked presets
|
|
||||||
*/
|
|
||||||
function PresetView() {
|
|
||||||
const { data, status } = useUrlPresets();
|
|
||||||
const { alias } = useParams();
|
|
||||||
|
|
||||||
const preset: URLPreset | undefined = useMemo(() => {
|
|
||||||
if (status === 'pending' || !alias) return;
|
|
||||||
return data.find((p) => p.alias === alias && p.enabled);
|
|
||||||
}, [data, status, alias]);
|
|
||||||
|
|
||||||
if (status === 'pending') {
|
|
||||||
return <Loader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need to check the session scope to determine if the user can navigate
|
|
||||||
* If the user has a global scope, they can navigate freely
|
|
||||||
* Otherwise, they are locked to the preset view
|
|
||||||
*/
|
|
||||||
const showNav = sessionScope === 'rw';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If we are in a preset path but cannot find a preset, we will need to show a not found page
|
|
||||||
* This can happen if the preset was deleted or disabled
|
|
||||||
*/
|
|
||||||
if (!preset) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewNavigationMenu isNavigationLocked={!showNav} suppressSettings />
|
|
||||||
<NotFound />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 />
|
|
||||||
{Component ? <Component /> : <NotFound />}
|
|
||||||
</PresetContext>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RedirectPreset() {
|
|
||||||
const { data, status } = useUrlPresets();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
// checks if we are in a preset path and resolves a destination URL
|
|
||||||
const destination = useMemo(() => {
|
|
||||||
if (status === 'pending') return null;
|
|
||||||
return getRouteFromPreset(location, data);
|
|
||||||
}, [data, location, status]);
|
|
||||||
|
|
||||||
// if we have a destination, we will navigate to it
|
|
||||||
useEffect(() => {
|
|
||||||
if (destination) {
|
|
||||||
navigate(`/${destination}`, { replace: true });
|
|
||||||
}
|
|
||||||
}, [destination, navigate]);
|
|
||||||
|
|
||||||
if (status === 'pending') {
|
|
||||||
return <Loader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings />
|
|
||||||
<NotFound />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { TranslationObject } from 'ontime-types';
|
|
||||||
|
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
|
||||||
import { TRANSLATION, apiEntryUrl, customTranslationsURL } 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 });
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to post css contents
|
|
||||||
*/
|
|
||||||
export async function postCSSContents(css: string): Promise<void> {
|
|
||||||
await axios.post(`${assetsPath}/css`, {
|
|
||||||
css,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to restore default css
|
|
||||||
*/
|
|
||||||
export async function restoreCSSContents(): Promise<string> {
|
|
||||||
const res = await axios.post(`${assetsPath}/css/restore`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to get user translation
|
|
||||||
*/
|
|
||||||
export async function getUserTranslation(options?: RequestOptions): Promise<TranslationObject> {
|
|
||||||
const res = await axios.get(customTranslationsURL, { signal: options?.signal });
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to post user translation
|
|
||||||
*/
|
|
||||||
export async function postUserTranslation(translation: TranslationObject): Promise<void> {
|
|
||||||
await axios.post(`${assetsPath}/translations`, {
|
|
||||||
translation,
|
|
||||||
});
|
|
||||||
await ontimeQueryClient.invalidateQueries({ queryKey: TRANSLATION });
|
|
||||||
}
|
|
||||||
@@ -9,15 +9,14 @@ import type {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const automationsPath = `${apiEntryUrl}/automations`;
|
const automationsPath = `${apiEntryUrl}/automations`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to get the automations settings
|
* HTTP request to get the automations settings
|
||||||
*/
|
*/
|
||||||
export async function getAutomationSettings(options?: RequestOptions): Promise<AutomationSettings> {
|
export async function getAutomationSettings(): Promise<AutomationSettings> {
|
||||||
const res = await axios.get(automationsPath, { signal: options?.signal });
|
const res = await axios.get(automationsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
|
|
||||||
axios.defaults.validateStatus = (status) => {
|
|
||||||
return status >= 200 && status < 300;
|
|
||||||
};
|
|
||||||
axios.defaults.timeout = axiosConfig.shortTimeout;
|
|
||||||
@@ -8,22 +8,22 @@ export const AUTOMATION = ['automation'];
|
|||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
export const PROJECT_DATA = ['project'];
|
export const PROJECT_DATA = ['project'];
|
||||||
export const PROJECT_LIST = ['projectList'];
|
export const PROJECT_LIST = ['projectList'];
|
||||||
export const PROJECT_RUNDOWNS = ['projectRundowns'];
|
|
||||||
export const RUNDOWN = ['rundown'];
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
|
export const SHEET_STATE = ['sheetState'];
|
||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
export const CLIENT_LIST = ['clientList'];
|
export const CLIENT_LIST = ['clientList'];
|
||||||
export const REPORT = ['report'];
|
export const REPORT = ['report'];
|
||||||
export const TRANSLATION = ['translation'];
|
|
||||||
|
|
||||||
// API URLs
|
// API URLs
|
||||||
export const apiEntryUrl = `${serverURL}/data`;
|
export const apiEntryUrl = `${serverURL}/data`;
|
||||||
|
|
||||||
const userAssetsPath = 'user';
|
export const projectDataURL = `${serverURL}/project`;
|
||||||
const cssOverridePath = 'styles/override.css';
|
export const rundownURL = `${serverURL}/events`;
|
||||||
const customTranslationsPath = 'translations/translations.json';
|
export const ontimeURL = `${serverURL}/ontime`;
|
||||||
|
|
||||||
|
export const userAssetsPath = 'user';
|
||||||
|
export const cssOverridePath = 'styles/override.css';
|
||||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
||||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||||
export const customTranslationsURL = `${serverURL}/${userAssetsPath}/${customTranslationsPath}`;
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
import { CustomField, CustomFieldLabel, CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests list of known custom fields
|
* Requests list of known custom fields
|
||||||
*/
|
*/
|
||||||
export async function getCustomFields(options?: RequestOptions): Promise<CustomFields> {
|
export async function getCustomFields(): Promise<CustomFields> {
|
||||||
const res = await axios.get(customFieldsPath, { signal: options?.signal });
|
const res = await axios.get(customFieldsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,15 +24,15 @@ export async function postCustomField(newField: CustomField): Promise<CustomFiel
|
|||||||
/**
|
/**
|
||||||
* Edits single custom field
|
* Edits single custom field
|
||||||
*/
|
*/
|
||||||
export async function editCustomField(key: CustomFieldKey, newField: CustomField): Promise<CustomFields> {
|
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
||||||
const res = await axios.put(`${customFieldsPath}/${key}`, { ...newField });
|
const res = await axios.put(`${customFieldsPath}/${label}`, { ...newField });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes single custom field
|
* Deletes single custom field
|
||||||
*/
|
*/
|
||||||
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFields> {
|
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
||||||
const res = await axios.delete(`${customFieldsPath}/${key}`);
|
const res = await axios.delete(`${customFieldsPath}/${label}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
|
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||||
|
import { makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
import { createBlob, downloadBlob } from './utils';
|
import { createBlob, downloadBlob } from './utils';
|
||||||
|
|
||||||
const dbPath = `${apiEntryUrl}/db`;
|
const dbPath = `${apiEntryUrl}/db`;
|
||||||
@@ -12,7 +13,7 @@ const dbPath = `${apiEntryUrl}/db`;
|
|||||||
* HTTP request to the current DB
|
* HTTP request to the current DB
|
||||||
*/
|
*/
|
||||||
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
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,25 @@ 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, rundown, customFields } = data;
|
||||||
|
|
||||||
|
const sheetData = makeTable(project, rundown, customFields);
|
||||||
|
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
||||||
|
|
||||||
|
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
||||||
|
downloadBlob(blob, `${name}.csv`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to upload project file
|
* HTTP request to upload project file
|
||||||
*/
|
*/
|
||||||
@@ -39,7 +59,6 @@ export async function uploadProjectFile(file: File): Promise<MessageResponse> {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('project', file);
|
formData.append('project', file);
|
||||||
const response = await axios.post(`${dbPath}/upload`, formData, {
|
const response = await axios.post(`${dbPath}/upload`, formData, {
|
||||||
timeout: axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -79,11 +98,8 @@ export async function quickProject(data: QuickStartData): Promise<MessageRespons
|
|||||||
/**
|
/**
|
||||||
* HTTP request to get the list of available project files
|
* HTTP request to get the list of available project files
|
||||||
*/
|
*/
|
||||||
export async function getProjects(options?: RequestOptions): Promise<ProjectFileListResponse> {
|
export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||||
const res = await axios.get(`${dbPath}/all`, {
|
const res = await axios.get(`${dbPath}/all`);
|
||||||
signal: options?.signal,
|
|
||||||
timeout: options?.timeout,
|
|
||||||
});
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +1,42 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
import { downloadBlob } from './utils';
|
|
||||||
|
|
||||||
const excelPath = `${apiEntryUrl}/excel`;
|
const excelPath = `${apiEntryUrl}/excel`;
|
||||||
|
|
||||||
|
type PreviewSpreadsheetResponse = {
|
||||||
|
rundown: OntimeRundown;
|
||||||
|
customFields: CustomFields;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* upload Excel file to server
|
* upload Excel file to server
|
||||||
* @return string - file ID op the uploaded file
|
* @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();
|
const formData = new FormData();
|
||||||
formData.append('excel', file);
|
formData.append('excel', file);
|
||||||
const response = await axios.post(`${excelPath}/upload`, formData, {
|
await axios.post(`${excelPath}/upload`, formData, {
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a xlsx representation of the rundown from the server
|
* Get Worksheet names
|
||||||
|
* @return string[] - array of available worksheets
|
||||||
*/
|
*/
|
||||||
export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) {
|
export async function getWorksheetNames(): Promise<string[]> {
|
||||||
try {
|
const response: AxiosResponse<string[]> = await axios.get(`${excelPath}/worksheets`);
|
||||||
const response = await axios.get(`${excelPath}/${rundownId}/export`, {
|
return response.data;
|
||||||
signal: requestOptions?.signal,
|
}
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
responseType: 'blob',
|
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||||
});
|
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||||
|
options,
|
||||||
downloadBlob(response.data, `${fileName ?? 'Ontime_rundown'}.xlsx`);
|
});
|
||||||
} catch (error) {
|
return response.data;
|
||||||
console.error('Error downloading file:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
import { apiRepoLatest } from '../../externals';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
export type HasUpdate = {
|
export type HasUpdate = {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -11,8 +10,8 @@ export type HasUpdate = {
|
|||||||
/**
|
/**
|
||||||
* HTTP request to get the latest version and url from github
|
* HTTP request to get the latest version and url from github
|
||||||
*/
|
*/
|
||||||
export async function getLatestVersion(options?: RequestOptions): Promise<HasUpdate> {
|
export async function getLatestVersion(): Promise<HasUpdate> {
|
||||||
const res = await axios.get(apiRepoLatest, { signal: options?.signal });
|
const res = await axios.get(`${apiRepoLatest}`);
|
||||||
return {
|
return {
|
||||||
url: res.data.html_url as string,
|
url: res.data.html_url as string,
|
||||||
version: res.data.tag_name 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 { ProjectData, ProjectLogoResponse } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
|
|
||||||
const projectPath = `${apiEntryUrl}/project`;
|
const projectPath = `${apiEntryUrl}/project`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch project data
|
* HTTP request to fetch project data
|
||||||
*/
|
*/
|
||||||
export async function getProjectData(options?: RequestOptions): Promise<ProjectData> {
|
export async function getProjectData(): Promise<ProjectData> {
|
||||||
const res = await axios.get(projectPath, {
|
const res = await axios.get(projectPath);
|
||||||
signal: options?.signal,
|
|
||||||
timeout: options?.timeout,
|
|
||||||
});
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +27,6 @@ export async function uploadProjectLogo(file: File): Promise<AxiosResponse<Proje
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', file);
|
formData.append('image', file);
|
||||||
const response = await axios.post(`${projectPath}/upload`, formData, {
|
const response = await axios.post(`${projectPath}/upload`, formData, {
|
||||||
timeout: axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import axios from 'axios';
|
|||||||
import { OntimeReport } from 'ontime-types';
|
import { OntimeReport } from 'ontime-types';
|
||||||
|
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
import { REPORT, apiEntryUrl } from './constants';
|
|
||||||
import type { RequestOptions } from './requestOptions';
|
import { apiEntryUrl, REPORT } from './constants';
|
||||||
|
|
||||||
export const reportUrl = `${apiEntryUrl}/report`;
|
export const reportUrl = `${apiEntryUrl}/report`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch all reports
|
* HTTP request to fetch all reports
|
||||||
*/
|
*/
|
||||||
export async function fetchReport(options?: RequestOptions): Promise<OntimeReport> {
|
export async function fetchReport(): Promise<OntimeReport> {
|
||||||
const res = await axios.get(reportUrl, { signal: options?.signal });
|
const res = await axios.get(`${reportUrl}/`);
|
||||||
return res.data;
|
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;
|
|
||||||
@@ -1,184 +1,86 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
|
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
type RundownId = string;
|
const rundownPath = `${apiEntryUrl}/rundown`;
|
||||||
const rundownPath = `${apiEntryUrl}/rundowns`;
|
|
||||||
|
|
||||||
// #region operations on project rundowns =========================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch a list of existing rundowns
|
* HTTP request to fetch all events
|
||||||
*/
|
*/
|
||||||
export async function fetchProjectRundownList(options?: RequestOptions): Promise<ProjectRundownsList> {
|
export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
||||||
const res = await axios.get(rundownPath, { signal: options?.signal });
|
const res = await axios.get(`${rundownPath}/normalised`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch all entries in the currently loaded rundown
|
* HTTP request to post new event
|
||||||
*/
|
*/
|
||||||
export async function fetchCurrentRundown(options?: RequestOptions): Promise<Rundown> {
|
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||||
const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal });
|
return axios.post(rundownPath, data);
|
||||||
if (!isValidRundown(res.data)) {
|
|
||||||
throw new Error('Invalid rundown payload');
|
|
||||||
}
|
|
||||||
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
|
* HTTP request to put new event
|
||||||
*/
|
*/
|
||||||
export async function loadRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
|
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||||
return axios.post(`${rundownPath}/${rundownId}/load`);
|
return axios.put(rundownPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type BatchEditEntry = {
|
||||||
* HTTP request to create a new rundown
|
|
||||||
*/
|
|
||||||
export async function createRundown(title: string): Promise<AxiosResponse<ProjectRundownsList>> {
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// #endregion operations on project rundowns ======================
|
|
||||||
// #region operations on rundown entries ==========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to post new entry
|
|
||||||
*/
|
|
||||||
export async function postAddEntry(
|
|
||||||
rundownId: RundownId,
|
|
||||||
data: TransientEventPayload,
|
|
||||||
): Promise<AxiosResponse<OntimeEntry>> {
|
|
||||||
return axios.post(`${rundownPath}/${rundownId}/entry`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to edit an entry
|
|
||||||
*/
|
|
||||||
export async function putEditEntry(
|
|
||||||
rundownId: RundownId,
|
|
||||||
data: Partial<OntimeEntry>,
|
|
||||||
): Promise<AxiosResponse<OntimeEntry>> {
|
|
||||||
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BatchEditEntry = {
|
|
||||||
data: Partial<OntimeEvent>;
|
data: Partial<OntimeEvent>;
|
||||||
ids: EntryId[];
|
ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to edit multiple events
|
* HTTP request to put multiple events
|
||||||
*/
|
*/
|
||||||
export async function putBatchEditEvents(rundownId: RundownId, data: BatchEditEntry): Promise<AxiosResponse<Rundown>> {
|
export async function requestBatchPutEvents(data: BatchEditEntry): Promise<AxiosResponse<MessageResponse>> {
|
||||||
return axios.put(`${rundownPath}/${rundownId}/batch`, data);
|
return axios.put(`${rundownPath}/batch`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReorderEntry = {
|
export type ReorderEntry = {
|
||||||
entryId: EntryId;
|
eventId: string;
|
||||||
destinationId: EntryId;
|
from: number;
|
||||||
order: 'before' | 'after' | 'insert';
|
to: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to reorder an entry
|
* HTTP request to reorder events
|
||||||
*/
|
*/
|
||||||
export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry): Promise<AxiosResponse<Rundown>> {
|
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||||
return axios.patch(`${rundownPath}/${rundownId}/reorder`, data);
|
return axios.patch(`${rundownPath}/reorder`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SwapEntry = {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to swap two events
|
* HTTP request to swap two events
|
||||||
*/
|
*/
|
||||||
export async function requestEventSwap(
|
export async function requestEventSwap(data: SwapEntry): Promise<AxiosResponse<MessageResponse>> {
|
||||||
rundownId: RundownId,
|
return axios.patch(`${rundownPath}/swap`, data);
|
||||||
from: EntryId,
|
|
||||||
to: EntryId,
|
|
||||||
): Promise<AxiosResponse<Rundown>> {
|
|
||||||
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to request application of delay
|
* HTTP request to request application of delay
|
||||||
*/
|
*/
|
||||||
export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId): Promise<AxiosResponse<Rundown>> {
|
export async function requestApplyDelay(eventId: string): Promise<AxiosResponse<MessageResponse>> {
|
||||||
return axios.patch(`${rundownPath}/${rundownId}/applydelay/${delayId}`);
|
return axios.patch(`${rundownPath}/applydelay/${eventId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request for cloning an entry
|
* HTTP request to delete given event
|
||||||
*/
|
*/
|
||||||
export async function postCloneEntry(
|
export async function requestDelete(eventIds: string[]): Promise<AxiosResponse<MessageResponse>> {
|
||||||
rundownId: RundownId,
|
return axios.delete(rundownPath, { data: { ids: eventIds } });
|
||||||
entryId: EntryId,
|
|
||||||
options?: { before?: EntryId; after?: EntryId },
|
|
||||||
): Promise<AxiosResponse<Rundown>> {
|
|
||||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request for grouping a list of entries into a group
|
* HTTP request to delete all events
|
||||||
*/
|
*/
|
||||||
export async function requestGroupEntries(rundownId: RundownId, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
export async function requestDeleteAll(): Promise<AxiosResponse<MessageResponse>> {
|
||||||
return axios.post(`${rundownPath}/${rundownId}/group`, { ids: entryIds });
|
return axios.delete(`${rundownPath}/all`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request for dissolving of a group
|
|
||||||
*/
|
|
||||||
export async function requestUngroup(rundownId: RundownId, 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>> {
|
|
||||||
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>> {
|
|
||||||
return axios.delete(`${rundownPath}/${rundownId}/all`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// #endregion operations on rundown entries =======================
|
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { GetInfo, LinkOptions } from 'ontime-types';
|
import { GetInfo } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const sessionPath = `${apiEntryUrl}/session`;
|
const sessionPath = `${apiEntryUrl}/session`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve application info
|
* HTTP request to retrieve application info
|
||||||
*/
|
*/
|
||||||
export async function getInfo(options?: RequestOptions): Promise<GetInfo> {
|
export async function getInfo(): Promise<GetInfo> {
|
||||||
const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal });
|
const res = await axios.get(`${sessionPath}/info`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to get a pre-authenticated URL
|
* HTTP request to get a pre-authenticated URL
|
||||||
*/
|
*/
|
||||||
export async function generateUrl(options: LinkOptions & { baseUrl: string; path: string }): Promise<string> {
|
export async function generateUrl(
|
||||||
const res = await axios.post(`${sessionPath}/url`, options);
|
baseUrl: string,
|
||||||
|
path: string,
|
||||||
|
lock: boolean,
|
||||||
|
authenticate: boolean,
|
||||||
|
): Promise<string> {
|
||||||
|
const res = await axios.post(`${sessionPath}/url`, { baseUrl, path, lock, authenticate });
|
||||||
return res.data.url;
|
return res.data.url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { PortInfo, Settings } from 'ontime-types';
|
import { Settings } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const settingsPath = `${apiEntryUrl}/settings`;
|
const settingsPath = `${apiEntryUrl}/settings`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve application settings
|
* HTTP request to retrieve application settings
|
||||||
*/
|
*/
|
||||||
export async function getSettings(options?: RequestOptions): Promise<Settings> {
|
export async function getSettings(): Promise<Settings> {
|
||||||
const res = await axios.get(settingsPath, { signal: options?.signal });
|
const res = await axios.get(settingsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,18 +26,3 @@ export async function postSettings(data: Settings): Promise<AxiosResponse<Settin
|
|||||||
export async function postShowWelcomeDialog(show: boolean) {
|
export async function postShowWelcomeDialog(show: boolean) {
|
||||||
axios.post(`${settingsPath}/welcomedialog`, { show });
|
axios.post(`${settingsPath}/welcomedialog`, { show });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to retrieve server port
|
|
||||||
*/
|
|
||||||
export async function getServerPort(): Promise<PortInfo> {
|
|
||||||
const res = await axios.get(`${settingsPath}/serverport`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to set server port
|
|
||||||
*/
|
|
||||||
export async function postServerPort(serverPort: number): Promise<AxiosResponse<PortInfo>> {
|
|
||||||
return axios.post(`${settingsPath}/serverport`, { serverPort });
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
import { axiosConfig } from './requestTimeouts';
|
|
||||||
|
|
||||||
const sheetsPath = `${apiEntryUrl}/sheets`;
|
const sheetsPath = `${apiEntryUrl}/sheets`;
|
||||||
|
|
||||||
@@ -25,7 +23,6 @@ export const verifyAuthenticationStatus = async (): Promise<{
|
|||||||
export const requestConnection = async (
|
export const requestConnection = async (
|
||||||
file: File,
|
file: File,
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
requestOptions?: RequestOptions,
|
|
||||||
): Promise<{
|
): Promise<{
|
||||||
verification_url: string;
|
verification_url: string;
|
||||||
user_code: string;
|
user_code: string;
|
||||||
@@ -34,8 +31,6 @@ export const requestConnection = async (
|
|||||||
formData.append('client_secret', file);
|
formData.append('client_secret', file);
|
||||||
|
|
||||||
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -58,46 +53,23 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
|
|||||||
export const previewRundown = async (
|
export const previewRundown = async (
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ImportMap,
|
options: ImportMap,
|
||||||
requestOptions?: RequestOptions,
|
|
||||||
): Promise<{
|
): Promise<{
|
||||||
rundown: Rundown;
|
rundown: OntimeRundown;
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
summary: RundownSummary;
|
|
||||||
}> => {
|
}> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||||
`${sheetsPath}/${sheetId}/read`,
|
|
||||||
{ options },
|
|
||||||
{
|
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise<string[]> => {
|
export const getWorksheetNames = async (sheetId: string): Promise<string[]> => {
|
||||||
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`, undefined, {
|
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`);
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
});
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to upload the rundown to a google sheet
|
* HTTP request to upload the rundown to a google sheet
|
||||||
*/
|
*/
|
||||||
export const uploadRundown = async (
|
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
||||||
sheetId: string,
|
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options });
|
||||||
options: ImportMap,
|
|
||||||
requestOptions?: RequestOptions,
|
|
||||||
): Promise<void> => {
|
|
||||||
const response = await axios.post(
|
|
||||||
`${sheetsPath}/${sheetId}/write`,
|
|
||||||
{ options },
|
|
||||||
{
|
|
||||||
signal: requestOptions?.signal,
|
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,35 +2,20 @@ import axios from 'axios';
|
|||||||
import { URLPreset } from 'ontime-types';
|
import { URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve all presets
|
* HTTP request to retrieve aliases
|
||||||
*/
|
*/
|
||||||
export async function getUrlPresets(options?: RequestOptions): Promise<URLPreset[]> {
|
export async function getUrlPresets(): Promise<URLPreset[]> {
|
||||||
const res = await axios.get(urlPresetsPath, { signal: options?.signal });
|
const res = await axios.get(urlPresetsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to add a preset
|
* HTTP request to mutate aliases
|
||||||
*/
|
*/
|
||||||
export async function postUrlPreset(data: URLPreset): Promise<URLPreset[]> {
|
export async function postUrlPresets(data: URLPreset[]): Promise<URLPreset[]> {
|
||||||
return (await axios.post(urlPresetsPath, data)).data;
|
return axios.post(urlPresetsPath, data);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to edit a preset
|
|
||||||
*/
|
|
||||||
export async function putUrlPreset(alias: string, data: URLPreset): Promise<URLPreset[]> {
|
|
||||||
return (await axios.put(`${urlPresetsPath}/${alias}`, data)).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to delete a preset
|
|
||||||
*/
|
|
||||||
export async function deleteUrlPreset(alias: string): Promise<URLPreset[]> {
|
|
||||||
return (await axios.delete(`${urlPresetsPath}/${alias}`)).data;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { addLog } from '../stores/logger';
|
|||||||
import { nowInMillis } from '../utils/time';
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility unwrap a potential axios error
|
* Utility unrwap a potential axios error
|
||||||
* @param error
|
* @param error
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@@ -34,21 +34,7 @@ export function maybeAxiosError(error: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility unwrap a an instance of Error
|
* Utility unrwaps a potential axios error and sends to logger
|
||||||
*/
|
|
||||||
export function unwrapError(error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
} else {
|
|
||||||
if (typeof error !== 'string') {
|
|
||||||
return JSON.stringify(error);
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility unwraps a potential axios error and sends to logger
|
|
||||||
* @param prepend
|
* @param prepend
|
||||||
* @param error
|
* @param error
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
|
||||||
|
|
||||||
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch view settings
|
* HTTP request to retrieve view settings
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export async function getViewSettings(options?: RequestOptions): Promise<ViewSettings> {
|
export async function getView(): Promise<ViewSettings> {
|
||||||
const res = await axios.get(viewSettingsPath, { signal: options?.signal });
|
const res = await axios.get(viewSettingsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to update view settings
|
* HTTP request to mutate view settings
|
||||||
* needs to update entire objects, not just a patch
|
|
||||||
*/
|
*/
|
||||||
export async function postViewSettings(data: ViewSettings): Promise<ViewSettings> {
|
export async function postViewSettings(data: ViewSettings) {
|
||||||
const res = await axios.post(viewSettingsPath, data);
|
return axios.post(viewSettingsPath, data);
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
@use './buttonVariants' as base;
|
|
||||||
|
|
||||||
@include base.button-variants;
|
|
||||||
|
|
||||||
.baseButton {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
width: fit-content;
|
|
||||||
padding-inline: 1em;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: $component-border-radius-md;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid $blue-500;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
cursor: wait;
|
|
||||||
.content {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingOverlay {
|
|
||||||
position: absolute;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
stroke-dasharray: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.small {
|
|
||||||
height: 1.5rem;
|
|
||||||
font-size: calc(1rem - 3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.medium {
|
|
||||||
height: 2rem;
|
|
||||||
font-size: calc(1rem - 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.large {
|
|
||||||
height: 2.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xlarge {
|
|
||||||
height: 3rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fluid {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
|
||||||
import { IoEllipseOutline } from 'react-icons/io5';
|
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
|
||||||
|
|
||||||
import style from './Button.module.scss';
|
|
||||||
|
|
||||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
||||||
variant?:
|
|
||||||
| 'primary'
|
|
||||||
| 'subtle'
|
|
||||||
| 'subtle-white'
|
|
||||||
| 'destructive'
|
|
||||||
| 'subtle-destructive'
|
|
||||||
| 'ghosted'
|
|
||||||
| 'ghosted-white'
|
|
||||||
| 'ghosted-destructive';
|
|
||||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
|
||||||
fluid?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ className, children, variant = 'subtle', size = 'medium', fluid, loading, ...buttonProps }, ref) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
className={cx([
|
|
||||||
style.baseButton,
|
|
||||||
style[variant],
|
|
||||||
style[size],
|
|
||||||
fluid && style.fluid,
|
|
||||||
loading && style.loading,
|
|
||||||
className,
|
|
||||||
])}
|
|
||||||
type='button'
|
|
||||||
disabled={loading || buttonProps.disabled}
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
<span className={style.content}>{children}</span>
|
|
||||||
{loading && (
|
|
||||||
<div className={style.loadingOverlay}>
|
|
||||||
<IoEllipseOutline className={style.spinner} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Button.displayName = 'Button';
|
|
||||||
|
|
||||||
export default Button;
|
|
||||||
@@ -1,45 +1,26 @@
|
|||||||
@use './buttonVariants' as base;
|
.subtle {
|
||||||
|
|
||||||
@include base.button-variants;
|
|
||||||
|
|
||||||
.baseIconButton {
|
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
height: 2rem;
|
||||||
|
height: 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $blue-400;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: $component-border-radius-md;
|
border-radius: 3px;
|
||||||
|
|
||||||
cursor: pointer;
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:active {
|
||||||
outline: 2px solid $blue-500;
|
background: $gray-1100;
|
||||||
outline-offset: 2px;
|
border-color: $gray-1250;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.4;
|
background: $gray-1050;
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
|
||||||
height: 1.5rem;
|
|
||||||
font-size: calc(1rem - 3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.medium {
|
|
||||||
height: 2rem;
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large {
|
|
||||||
height: 2.5rem;
|
|
||||||
width: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xlarge {
|
|
||||||
height: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,37 +1,14 @@
|
|||||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
import { ButtonHTMLAttributes } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './IconButton.module.scss';
|
import style from './IconButton.module.scss';
|
||||||
|
|
||||||
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export default function IconButton(props: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||||
variant?:
|
const { className, children, ...buttonProps } = props;
|
||||||
| 'primary'
|
return (
|
||||||
| 'subtle'
|
<button className={cx([style.subtle, className])} {...buttonProps}>
|
||||||
| 'subtle-white'
|
{children}
|
||||||
| 'destructive'
|
</button>
|
||||||
| 'subtle-destructive'
|
);
|
||||||
| 'ghosted'
|
|
||||||
| 'ghosted-white'
|
|
||||||
| 'ghosted-destructive';
|
|
||||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
||||||
({ className, children, variant = 'subtle', size = 'medium', ...buttonProps }, ref) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
className={cx([style.baseIconButton, style[variant], style[size], className])}
|
|
||||||
type='button'
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
IconButton.displayName = 'IconButton';
|
|
||||||
|
|
||||||
export default IconButton;
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { MouseEvent } from 'react';
|
||||||
|
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
interface TooltipActionBtnProps extends IconButtonProps {
|
||||||
|
clickHandler: (event: MouseEvent) => void | Promise<void>;
|
||||||
|
tooltip: string;
|
||||||
|
openDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TooltipActionBtn(props: TooltipActionBtnProps) {
|
||||||
|
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Tooltip label={tooltip} openDelay={openDelay}>
|
||||||
|
<IconButton {...rest} size={size} icon={icon} onClick={clickHandler} className={className} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,50 +0,0 @@
|
|||||||
.checkbox {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: $component-border-radius-sm;
|
|
||||||
|
|
||||||
&[data-unchecked] {
|
|
||||||
background-color: $gray-1100;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background: $gray-1000;
|
|
||||||
}
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-checked] {
|
|
||||||
background-color: $blue-500;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background: $blue-400;
|
|
||||||
}
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $blue-600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid $blue-500;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator {
|
|
||||||
display: flex;
|
|
||||||
color: $ui-white;
|
|
||||||
|
|
||||||
&[data-unchecked] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
|
||||||
import { IoCheckmark } from 'react-icons/io5';
|
|
||||||
|
|
||||||
import style from './Checkbox.module.scss';
|
|
||||||
|
|
||||||
export default function Checkbox(checkboxProps: BaseCheckbox.Root.Props) {
|
|
||||||
return (
|
|
||||||
<BaseCheckbox.Root className={style.checkbox} {...checkboxProps}>
|
|
||||||
<BaseCheckbox.Indicator className={style.indicator}>
|
|
||||||
<IoCheckmark className={style.icon} />
|
|
||||||
</BaseCheckbox.Indicator>
|
|
||||||
</BaseCheckbox.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoArrowForward } from 'react-icons/io5';
|
import {
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Select,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { IoArrowForward } from '@react-icons/all-files/io5/IoArrowForward';
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
import Button from '../buttons/Button';
|
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||||
import Dialog from '../dialog/Dialog';
|
|
||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
import Input from '../input/input/Input';
|
|
||||||
import AppLink from '../link/app-link/AppLink';
|
import AppLink from '../link/app-link/AppLink';
|
||||||
import Select from '../select/Select';
|
|
||||||
|
|
||||||
import style from './RedirectClientModal.module.scss';
|
import style from './RedirectClientModal.module.scss';
|
||||||
|
|
||||||
@@ -22,7 +29,8 @@ interface RedirectClientModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
|
export function RedirectClientModal(props: RedirectClientModalProps) {
|
||||||
|
const { id, isOpen, name, currentPath, origin, onClose } = props;
|
||||||
const { data } = useUrlPresets();
|
const { data } = useUrlPresets();
|
||||||
const [path, setPath] = useState(currentPath);
|
const [path, setPath] = useState(currentPath);
|
||||||
const [selected, setSelected] = useState('/');
|
const [selected, setSelected] = useState('/');
|
||||||
@@ -33,91 +41,86 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
if (newPath === '/' || newPath === currentPath) {
|
if (newPath === '/' || newPath === currentPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setRedirect({ target: id, redirect: newPath });
|
||||||
if (newPath.startsWith('preset-')) {
|
|
||||||
setRedirect({ target: id, redirect: newPath.slice(7) });
|
|
||||||
} else {
|
|
||||||
setRedirect({ target: id, redirect: newPath });
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const enabledPresets = data.filter((preset) => preset.enabled);
|
const enabledPresets = data.filter((preset) => preset.enabled);
|
||||||
|
|
||||||
const viewOptions = [
|
|
||||||
...navigatorConstants.map((view) => ({
|
|
||||||
value: `/${view.url}`,
|
|
||||||
label: view.label,
|
|
||||||
})),
|
|
||||||
...enabledPresets.map((preset) => ({
|
|
||||||
value: `preset-${preset.alias}`,
|
|
||||||
label: `URL Preset: ${preset.alias}`,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||||
isOpen={isOpen}
|
<ModalOverlay />
|
||||||
onClose={onClose}
|
<ModalContent maxWidth='max(480px, 35vw)'>
|
||||||
showCloseButton
|
<ModalHeader>Redirect: {name}</ModalHeader>
|
||||||
showBackdrop
|
<ModalCloseButton />
|
||||||
title={`Redirect: ${name}`}
|
<ModalBody>
|
||||||
bodyElements={
|
|
||||||
<>
|
|
||||||
<Info>
|
<Info>
|
||||||
Remotely redirect the client to a different URL. <br />
|
Remotely redirect the client to a different URL. <br />
|
||||||
Either by entering a custom path or selecting a URL Preset.
|
Either by selecting a URL Preset or entering a custom path.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<AppLink search='settings=sharing__presets'>Manage URL Presets</AppLink>
|
<AppLink search='settings=feature_settings__urlpresets'>Manage URL Presets</AppLink>
|
||||||
</Info>
|
</Info>
|
||||||
|
<div>
|
||||||
|
<span className={style.label}>Select View or URL Preset</span>
|
||||||
|
<div className={style.textEntry}>
|
||||||
|
<Select
|
||||||
|
size='md'
|
||||||
|
variant='ontime'
|
||||||
|
isDisabled={enabledPresets.length === 0}
|
||||||
|
onChange={(event) => setSelected(event.target.value)}
|
||||||
|
>
|
||||||
|
<option value='/'>Select view or preset</option>
|
||||||
|
{navigatorConstants.map((view) => {
|
||||||
|
return (
|
||||||
|
<option key={view.url} value={`/${view.url}`}>
|
||||||
|
{view.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{enabledPresets.map((preset) => {
|
||||||
|
return (
|
||||||
|
<option key={preset.pathAndParams} value={preset.pathAndParams}>
|
||||||
|
{`Preset: ${preset.alias}`}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
<IconButton
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='md'
|
||||||
|
aria-label='Redirect to preset'
|
||||||
|
className={style.redirect}
|
||||||
|
icon={<IoArrowForward />}
|
||||||
|
isDisabled={enabledPresets.length === 0 || selected === '/'}
|
||||||
|
onClick={() => handleRedirect(selected)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className={style.inlineEntry}>
|
<div className={style.inlineEntry}>
|
||||||
<span className={style.label}>Enter custom path</span>
|
<span className={style.label}>Enter custom path</span>
|
||||||
<label className={style.textEntry}>
|
<label className={style.textEntry}>
|
||||||
{origin}
|
{origin}
|
||||||
<Input placeholder='eg. /timer' fluid value={path} onChange={(event) => setPath(event.target.value)} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='md'
|
||||||
|
placeholder='eg. /minimal?key=0000ffff'
|
||||||
|
value={path}
|
||||||
|
onChange={(event) => setPath(event.target.value)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<IconButton
|
||||||
variant='primary'
|
variant='ontime-filled'
|
||||||
|
size='md'
|
||||||
aria-label='Redirect'
|
aria-label='Redirect'
|
||||||
disabled={path === currentPath || path === ''}
|
isDisabled={path === currentPath || path === ''}
|
||||||
className={style.redirect}
|
className={style.redirect}
|
||||||
|
icon={<IoArrowForward />}
|
||||||
onClick={() => handleRedirect(path)}
|
onClick={() => handleRedirect(path)}
|
||||||
>
|
/>
|
||||||
Redirect
|
|
||||||
<IoArrowForward />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</ModalBody>
|
||||||
<span className={style.label}>Select View or URL Preset</span>
|
</ModalContent>
|
||||||
<div className={style.inlineEntry}>
|
</Modal>
|
||||||
<label className={style.textEntry}>
|
|
||||||
{origin}
|
|
||||||
<Select
|
|
||||||
fluid
|
|
||||||
options={viewOptions}
|
|
||||||
defaultValue={viewOptions[0].value}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value === null) return;
|
|
||||||
setSelected(value);
|
|
||||||
}}
|
|
||||||
disabled={enabledPresets.length === 0}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<Button
|
|
||||||
variant='primary'
|
|
||||||
aria-label='Redirect to preset'
|
|
||||||
className={style.redirect}
|
|
||||||
disabled={enabledPresets.length === 0 || selected === '/'}
|
|
||||||
onClick={() => handleRedirect(selected)}
|
|
||||||
>
|
|
||||||
Redirect <IoArrowForward />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import Button from '../../../common/components/buttons/Button';
|
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
import Dialog from '../dialog/Dialog';
|
|
||||||
import Input from '../input/input/Input';
|
|
||||||
|
|
||||||
interface RenameClientModalProps {
|
interface RenameClientModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,7 +20,8 @@ interface RenameClientModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenameClientModal({ id, name: currentName = '', isOpen, onClose }: RenameClientModalProps) {
|
export function RenameClientModal(props: RenameClientModalProps) {
|
||||||
|
const { id, name: currentName = '', isOpen, onClose } = props;
|
||||||
const [name, setName] = useState(currentName);
|
const [name, setName] = useState(currentName);
|
||||||
|
|
||||||
const { setClientName } = setClientRemote;
|
const { setClientName } = setClientRemote;
|
||||||
@@ -27,24 +36,29 @@ export function RenameClientModal({ id, name: currentName = '', isOpen, onClose
|
|||||||
const canSubmit = name !== currentName && name !== '';
|
const canSubmit = name !== currentName && name !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||||
isOpen={isOpen}
|
<ModalOverlay />
|
||||||
title={`Rename Client: ${currentName}`}
|
<ModalContent>
|
||||||
showCloseButton
|
<ModalHeader>Rename: {currentName}</ModalHeader>
|
||||||
onClose={onClose}
|
<ModalCloseButton />
|
||||||
bodyElements={
|
<ModalBody>
|
||||||
<Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
|
<Input
|
||||||
}
|
variant='ontime-filled'
|
||||||
footerElements={
|
size='md'
|
||||||
<>
|
placeholder='new name'
|
||||||
<Button variant='subtle' size='large' onClick={onClose}>
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='primary' size='large' onClick={handleRename} disabled={!canSubmit}>
|
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</ModalFooter>
|
||||||
}
|
</ModalContent>
|
||||||
/>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.contextMenuButton {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contextMenuBackdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// logic (with some modifications) culled from:
|
||||||
|
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
||||||
|
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
||||||
|
import { IconType } from '@react-icons/all-files';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { ContextMenuOption } from './ContextMenuOption';
|
||||||
|
|
||||||
|
import style from './ContextMenu.module.scss';
|
||||||
|
|
||||||
|
type ContextMenuCoords = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OptionWithoutGroup = {
|
||||||
|
label: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
icon: IconType;
|
||||||
|
onClick: () => void;
|
||||||
|
withDivider?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OptionWithGroup = {
|
||||||
|
label: string;
|
||||||
|
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Option = OptionWithoutGroup | OptionWithGroup;
|
||||||
|
|
||||||
|
const isOptionWithGroup = (option: Option): option is OptionWithGroup => 'group' in option;
|
||||||
|
|
||||||
|
type ContextMenuStore = {
|
||||||
|
coords: ContextMenuCoords;
|
||||||
|
options: Option[];
|
||||||
|
isOpen: boolean;
|
||||||
|
setContextMenu: (coords: ContextMenuCoords, options: Option[]) => void;
|
||||||
|
setIsOpen: (newIsOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
|
||||||
|
coords: { x: 0, y: 0 },
|
||||||
|
options: [],
|
||||||
|
isOpen: false,
|
||||||
|
setContextMenu: (coords, options) => set(() => ({ coords, options, isOpen: true })),
|
||||||
|
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
// ReactElement type required due to early `return` (line 51) returning {children}
|
||||||
|
children: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextMenu = ({ children }: ContextMenuProps) => {
|
||||||
|
const { coords, options, isOpen, setIsOpen } = useContextMenuStore();
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
return setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<div className={style.contextMenuBackdrop} />
|
||||||
|
<Menu isOpen size='sm' gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
||||||
|
<MenuButton
|
||||||
|
className={style.contextMenuButton}
|
||||||
|
aria-hidden
|
||||||
|
w={1}
|
||||||
|
h={1}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: coords.x,
|
||||||
|
top: coords.y,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
{options.map((option) =>
|
||||||
|
isOptionWithGroup(option) ? (
|
||||||
|
<MenuGroup key={option.label} title={option.label}>
|
||||||
|
{option.group.map((groupOption) => (
|
||||||
|
<ContextMenuOption key={groupOption.label} {...groupOption} />
|
||||||
|
))}
|
||||||
|
</MenuGroup>
|
||||||
|
) : (
|
||||||
|
<ContextMenuOption key={option.label} {...option} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { MenuDivider, MenuItem } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { OptionWithoutGroup } from './ContextMenu';
|
||||||
|
|
||||||
|
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
|
||||||
|
<>
|
||||||
|
{withDivider && <MenuDivider />}
|
||||||
|
<MenuItem icon={<Icon style={{ fontSize: '1rem' }} />} onClick={onClick} isDisabled={isDisabled}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
.copytag {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
border-radius: 3px 0 0 3px;
|
|
||||||
border: 1px solid $gray-1200;
|
|
||||||
background-color: $white-3;
|
|
||||||
padding-inline: 0.5rem 1rem;
|
|
||||||
line-height: 1em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: $label-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small {
|
|
||||||
height: 1.5rem;
|
|
||||||
font-size: calc(1rem - 3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.medium {
|
|
||||||
height: 2rem;
|
|
||||||
font-size: calc(1rem - 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.large {
|
|
||||||
height: 2.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,47 @@
|
|||||||
import { PropsWithChildren, useRef, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
import { IoCheckmark, IoCopy } from 'react-icons/io5';
|
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||||
|
import { IoCopy } from '@react-icons/all-files/io5/IoCopy';
|
||||||
|
|
||||||
import { copyToClipboard } from '../../utils/copyToClipboard';
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { Size } from '../../models/Util.type';
|
||||||
import Button from '../buttons/Button';
|
import copyToClipboard from '../../utils/copyToClipboard';
|
||||||
import IconButton from '../buttons/IconButton';
|
|
||||||
|
|
||||||
import style from './CopyTag.module.scss';
|
|
||||||
|
|
||||||
interface CopyTagProps {
|
interface CopyTagProps {
|
||||||
copyValue: string;
|
copyValue: string;
|
||||||
|
label: string;
|
||||||
|
size?: Size;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
size?: 'small' | 'medium' | 'large';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyTag({
|
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
|
||||||
copyValue,
|
const { copyValue, label, size = 'xs', disabled, children, onClick } = props;
|
||||||
disabled,
|
|
||||||
size = 'medium',
|
|
||||||
children,
|
|
||||||
onClick,
|
|
||||||
}: PropsWithChildren<CopyTagProps>) {
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = () => {
|
||||||
try {
|
copyToClipboard(copyValue);
|
||||||
await copyToClipboard(copyValue);
|
setCopied(true);
|
||||||
setCopied(true);
|
|
||||||
|
|
||||||
// reset copied state
|
// reset copied state
|
||||||
if (timeoutRef.current) {
|
setTimeout(() => setCopied(false), 1000);
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
|
||||||
} catch {
|
|
||||||
// ignore errors
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.copytag}>
|
<Tooltip label={label} openDelay={tooltipDelayFast}>
|
||||||
{onClick !== undefined ? (
|
<ButtonGroup size={size} isAttached>
|
||||||
<Button className={style.action} size={size} onClick={onClick} disabled={disabled}>
|
<Button variant='ontime-subtle' tabIndex={-1} onClick={onClick} isDisabled={disabled}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
<IconButton
|
||||||
<div className={cx([style.label, style[size]])}>{children}</div>
|
aria-label={label}
|
||||||
)}
|
icon={copied ? <IoCheckmark /> : <IoCopy />}
|
||||||
<IconButton className={style.copy} variant='primary' size={size} onClick={handleClick} disabled={disabled}>
|
variant='ontime-filled'
|
||||||
{copied ? <IoCheckmark /> : <IoCopy />}
|
tabIndex={-1}
|
||||||
</IconButton>
|
onClick={handleClick}
|
||||||
</div>
|
isDisabled={disabled}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
.delaySymbol {
|
.delaySymbol {
|
||||||
svg {
|
svg {
|
||||||
display: inline;
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: $ontime-delay;
|
color: $ontime-delay;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { IoChevronDown, IoChevronUp } from 'react-icons/io5';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
|
||||||
|
import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp';
|
||||||
|
|
||||||
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
import { millisToDelayString } from '../../utils/dateConfig';
|
import { millisToDelayString } from '../../utils/dateConfig';
|
||||||
import Tooltip from '../tooltip/Tooltip';
|
|
||||||
|
|
||||||
import style from './DelayIndicator.module.scss';
|
import style from './DelayIndicator.module.scss';
|
||||||
|
|
||||||
@@ -22,8 +24,8 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
|
|||||||
: millisToDelayString(delayValue);
|
: millisToDelayString(delayValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip text={delayString} render={<span />} className={style.delaySymbol}>
|
<Tooltip openDelay={tooltipDelayFast} label={delayString}>
|
||||||
{delayValue < 0 ? <IoChevronDown /> : <IoChevronUp />}
|
<span className={style.delaySymbol}>{delayValue < 0 ? <IoChevronDown /> : <IoChevronUp />}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
.dialog {
|
|
||||||
position: fixed;
|
|
||||||
top: 10%;
|
|
||||||
left: 50%;
|
|
||||||
|
|
||||||
transform: translateX(-50%);
|
|
||||||
|
|
||||||
padding-inline: 1rem;
|
|
||||||
min-width: min(600px, 90vw);
|
|
||||||
|
|
||||||
background-color: $gray-1250;
|
|
||||||
color: $ui-white;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: $box-shadow-l1;
|
|
||||||
border: 1px solid $gray-1100;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-color: $backdrop-color;
|
|
||||||
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
|
||||||
|
|
||||||
&[data-starting-style],
|
|
||||||
&[data-ending-style] {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding-block: 1rem;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
max-height: min(80vh, 600px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
padding-block: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: end;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { IoClose } from 'react-icons/io5';
|
|
||||||
|
|
||||||
import IconButton from '../buttons/IconButton';
|
|
||||||
|
|
||||||
import style from './Dialog.module.scss';
|
|
||||||
|
|
||||||
interface DialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
title: string;
|
|
||||||
showCloseButton?: boolean;
|
|
||||||
showBackdrop?: boolean;
|
|
||||||
bodyElements: ReactNode;
|
|
||||||
footerElements?: ReactNode;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dialog({
|
|
||||||
isOpen,
|
|
||||||
title,
|
|
||||||
showCloseButton,
|
|
||||||
showBackdrop,
|
|
||||||
bodyElements,
|
|
||||||
footerElements,
|
|
||||||
onClose,
|
|
||||||
}: DialogProps) {
|
|
||||||
return (
|
|
||||||
<BaseDialog.Root
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
if (!isOpen) onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BaseDialog.Portal>
|
|
||||||
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
|
||||||
<BaseDialog.Popup className={style.dialog}>
|
|
||||||
<div className={style.title}>
|
|
||||||
{title}
|
|
||||||
{showCloseButton && (
|
|
||||||
<IconButton variant='subtle-white' onClick={onClose}>
|
|
||||||
<IoClose />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={style.body}>{bodyElements}</div>
|
|
||||||
<div className={style.footer}>{footerElements}</div>
|
|
||||||
</BaseDialog.Popup>
|
|
||||||
</BaseDialog.Portal>
|
|
||||||
</BaseDialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
.positioner {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-block: 0.25rem;
|
|
||||||
|
|
||||||
color: $ui-white;
|
|
||||||
background-color: $gray-1250;
|
|
||||||
font-size: calc(1rem - 2px);
|
|
||||||
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid $gray-1000;
|
|
||||||
box-shadow: $box-shadow-l1;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
transform-origin: var(--transform-origin);
|
|
||||||
transition:
|
|
||||||
transform 150ms,
|
|
||||||
opacity 150ms;
|
|
||||||
|
|
||||||
&[data-starting-style],
|
|
||||||
&[data-ending-style] {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
outline: 0;
|
|
||||||
cursor: default;
|
|
||||||
padding-block: 0.5rem;
|
|
||||||
padding-inline: 1rem;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: $gray-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-disabled] {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-highlighted] {
|
|
||||||
z-index: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-highlighted]:not([data-disabled])::before {
|
|
||||||
content: '';
|
|
||||||
z-index: -1;
|
|
||||||
position: absolute;
|
|
||||||
inset-block: 0;
|
|
||||||
inset-inline: 0.25rem;
|
|
||||||
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 {
|
|
||||||
margin: 0.25rem 0.75rem;
|
|
||||||
height: 1px;
|
|
||||||
background-color: $white-7;
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
|
||||||
import { PropsWithChildren } from 'react';
|
|
||||||
import { IconType } from 'react-icons';
|
|
||||||
|
|
||||||
import style from './DropdownMenu.module.scss';
|
|
||||||
|
|
||||||
type DropdownMenuItemDivider = { type: 'divider' };
|
|
||||||
type DropdownMenuItem = {
|
|
||||||
type: 'item' | 'destructive';
|
|
||||||
label: string;
|
|
||||||
description?: string;
|
|
||||||
icon?: IconType;
|
|
||||||
disabled?: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
shortcut?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DropdownMenuOption = DropdownMenuItemDivider | DropdownMenuItem;
|
|
||||||
|
|
||||||
interface DropdownMenuProps extends BaseMenu.Trigger.Props {
|
|
||||||
items: DropdownMenuOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChildren<DropdownMenuProps>) {
|
|
||||||
return (
|
|
||||||
<BaseMenu.Root>
|
|
||||||
<BaseMenu.Trigger {...triggerProps}>{children}</BaseMenu.Trigger>
|
|
||||||
<BaseMenu.Portal>
|
|
||||||
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8}>
|
|
||||||
<BaseMenu.Popup className={style.popup}>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
if (item.type === 'divider') {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</BaseMenu.Popup>
|
|
||||||
</BaseMenu.Positioner>
|
|
||||||
</BaseMenu.Portal>
|
|
||||||
</BaseMenu.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PositionedDropdownMenuProps {
|
|
||||||
items: Array<DropdownMenuItemDivider | DropdownMenuItem>;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
position: { x: number; y: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PositionedDropdownMenu({ items, isOpen, position, onClose }: PositionedDropdownMenuProps) {
|
|
||||||
return (
|
|
||||||
<BaseMenu.Root
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BaseMenu.Trigger style={{ position: 'fixed', left: position.x, top: position.y }} aria-hidden />
|
|
||||||
<BaseMenu.Portal>
|
|
||||||
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8} alignOffset={8}>
|
|
||||||
<BaseMenu.Popup className={style.popup}>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
if (item.type === 'divider') {
|
|
||||||
return <BaseMenu.Separator key={index} className={style.separator} />;
|
|
||||||
}
|
|
||||||
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>}
|
|
||||||
</BaseMenu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</BaseMenu.Popup>
|
|
||||||
</BaseMenu.Positioner>
|
|
||||||
</BaseMenu.Portal>
|
|
||||||
</BaseMenu.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { HTMLAttributes, JSX, LabelHTMLAttributes, MouseEventHandler } 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 Title({ children, className, ...elementProps }: HTMLAttributes<HTMLHeadingElement>) {
|
|
||||||
const classes = cx([style.title, className]);
|
|
||||||
return (
|
|
||||||
<h3 className={classes} {...elementProps}>
|
|
||||||
{children}
|
|
||||||
</h3>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Label({ children, className, ...elementProps }: LabelHTMLAttributes<HTMLLabelElement>) {
|
|
||||||
const classes = cx([style.label, className]);
|
|
||||||
return (
|
|
||||||
<label className={classes} {...elementProps}>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
|
|
||||||
orientation?: 'horizontal' | 'vertical';
|
|
||||||
}
|
|
||||||
|
|
||||||
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} />;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
|
||||||
import * as Sentry from '@sentry/react';
|
|
||||||
/* eslint-disable react/destructuring-assignment */
|
/* eslint-disable react/destructuring-assignment */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
|
||||||
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
|
import { runtimeStore } from '@/common/stores/runtime';
|
||||||
import { runtimeStore } from '../../stores/runtime';
|
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
|
|
||||||
@@ -26,22 +26,17 @@ class ErrorBoundary extends React.Component {
|
|||||||
error,
|
error,
|
||||||
errorInfo: info,
|
errorInfo: info,
|
||||||
});
|
});
|
||||||
this.reportContent = `${error} ${info.componentStack}`;
|
|
||||||
|
|
||||||
const appState = runtimeStore.getState();
|
|
||||||
if (appState.ping < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Sentry.withScope((scope) => {
|
Sentry.withScope((scope) => {
|
||||||
scope.setExtras({
|
scope.setExtras({
|
||||||
error,
|
error,
|
||||||
store: appState,
|
store: runtimeStore.getState(),
|
||||||
hasSocket: { hasConnected, reconnectAttempts },
|
hasSocket: { hasConnected, shouldReconnect, reconnectAttempts },
|
||||||
});
|
});
|
||||||
const eventId = Sentry.captureException(error);
|
const eventId = Sentry.captureException(error);
|
||||||
this.setState({ eventId, info });
|
this.setState({ eventId, info });
|
||||||
});
|
});
|
||||||
|
this.reportContent = `${error} ${info.componentStack}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -49,14 +44,15 @@ class ErrorBoundary extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className={style.errorContainer} data-testid='error-container'>
|
<div className={style.errorContainer} data-testid='error-container'>
|
||||||
<div>
|
<div>
|
||||||
<p className={style.error}>: /</p>
|
<p className={style.error}>:/</p>
|
||||||
<p>Something went wrong.</p>
|
<p>Something went wrong</p>
|
||||||
<a
|
<div
|
||||||
|
role='button'
|
||||||
className={style.report}
|
className={style.report}
|
||||||
href={`mailto:mail@getontime.no?subject=Error%20Report&body=${encodeURIComponent(this.reportContent)}`}
|
onClick={() => Sentry.showReportDialog({ eventId: this.state.eventId })}
|
||||||
>
|
>
|
||||||
Report error
|
Report error
|
||||||
</a>
|
</div>
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
className={style.report}
|
className={style.report}
|
||||||
|
|||||||
@@ -3,21 +3,24 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
background-color: $ui-black;
|
background-color: #121212;
|
||||||
color: $ui-white;
|
color: white;
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: $error-red;
|
color: $error-red;
|
||||||
}
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.report {
|
.report {
|
||||||
color: $blue-500;
|
text-decoration: underline $error-red;
|
||||||
text-decoration: underline;
|
cursor: pointer;
|
||||||
text-underline-offset: 2px;
|
}
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
.report:hover {
|
||||||
color: $ontime-color;
|
color: $error-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report:active {
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,8 @@ interface FitTextProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
max?: number; // inclusive
|
max?: number; // inclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FitText({
|
export function FitText(props: PropsWithChildren<FitTextProps>) {
|
||||||
children,
|
const { children, mode = 'multi', min = 16, max = 256, ...elementProps } = props;
|
||||||
mode = 'multi',
|
|
||||||
min = 16,
|
|
||||||
max = 256,
|
|
||||||
...elementProps
|
|
||||||
}: PropsWithChildren<FitTextProps>) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const isOverflown = useCallback(() => {
|
const isOverflown = useCallback(() => {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { IconBaseProps } from 'react-icons';
|
|
||||||
import { IoLink } from 'react-icons/io5';
|
|
||||||
|
|
||||||
export default function RotatedLink(linkProps: IconBaseProps) {
|
|
||||||
return <IoLink style={{ transform: 'rotate(-45deg)' }} {...linkProps} />;
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
z-index: 100;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||||
|
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
|
|||||||
@@ -7,29 +7,15 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: $text-body-size;
|
font-size: $text-body-size;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
color: $gray-200;
|
|
||||||
|
.content {
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
min-width: 1.5rem;
|
min-width: 1.5rem;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
svg {
|
|
||||||
color: $info-blue;
|
color: $info-blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
|
||||||
svg {
|
|
||||||
color: $orange-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
svg {
|
|
||||||
color: $red-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { IoAlertCircle, IoWarning } from 'react-icons/io5';
|
import { IoAlertCircle } from '@react-icons/all-files/io5/IoAlertCircle';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
@@ -7,15 +7,14 @@ import style from './Info.module.scss';
|
|||||||
|
|
||||||
interface InfoProps {
|
interface InfoProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
type?: 'info' | 'warning' | 'error';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Info({ className, type = 'info', children }: PropsWithChildren<InfoProps>) {
|
export default function Info(props: PropsWithChildren<InfoProps>) {
|
||||||
|
const { className, children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.infoLabel, style[type], className])}>
|
<div className={cx([style.infoLabel, className])}>
|
||||||
{type === 'info' && <IoAlertCircle />}
|
<IoAlertCircle />
|
||||||
{type === 'warning' && <IoWarning />}
|
|
||||||
{type === 'error' && <IoWarning />}
|
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
||||||
|
// @ts-expect-error no types from library
|
||||||
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
|
||||||
|
export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknown> }) => {
|
||||||
|
const { value, inputref } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = inputref.current;
|
||||||
|
autosize(inputref.current);
|
||||||
|
return () => {
|
||||||
|
autosize.destroy(node);
|
||||||
|
};
|
||||||
|
}, [inputref, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
overflow='hidden'
|
||||||
|
w='100%'
|
||||||
|
ref={inputref}
|
||||||
|
resize='none'
|
||||||
|
transition='height none'
|
||||||
|
variant='ontime-transparent'
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// @ts-expect-error no types from library
|
|
||||||
import autosize from 'autosize/dist/autosize';
|
|
||||||
import { RefObject, useEffect } from 'react';
|
|
||||||
|
|
||||||
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
|
||||||
|
|
||||||
interface AutoTextAreaProps extends TextareaProps {
|
|
||||||
inputref: RefObject<HTMLTextAreaElement | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A textarea that automatically resizes based on its content
|
|
||||||
*/
|
|
||||||
export function AutoTextarea({ value, inputref, ...textAreaProps }: AutoTextAreaProps) {
|
|
||||||
// when the value changes, we use the ref to reapply autosize
|
|
||||||
useEffect(() => {
|
|
||||||
const node = inputref.current;
|
|
||||||
autosize(inputref.current);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
autosize.destroy(node);
|
|
||||||
};
|
|
||||||
}, [inputref, value]);
|
|
||||||
|
|
||||||
return <Textarea ref={inputref} value={value} {...textAreaProps} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||||
import { IoBan } from 'react-icons/io5';
|
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
@@ -11,13 +10,12 @@ interface SwatchProps {
|
|||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Swatch({ color, isSelected, onClick }: SwatchProps) {
|
export default function Swatch(props: SwatchProps) {
|
||||||
const handleClick = (event: MouseEvent) => {
|
const { color, isSelected, onClick } = props;
|
||||||
onClick?.(color);
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onClick?.(color);
|
||||||
|
};
|
||||||
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
||||||
|
|
||||||
if (!color) {
|
if (!color) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useDebouncedCallback } from '@mantine/hooks';
|
import { useCallback } from 'react';
|
||||||
|
import { useController, UseControllerProps } from 'react-hook-form';
|
||||||
|
import { IoEyedrop } from '@react-icons/all-files/io5/IoEyedrop';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
import { UseControllerProps, useController } from 'react-hook-form';
|
|
||||||
import { IoEyedrop } from 'react-icons/io5';
|
|
||||||
|
|
||||||
|
import { debounce } from '../../../utils/debounce';
|
||||||
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
||||||
import PopoverPicker from '../popover-picker/PopoverPicker';
|
import PopoverPicker from '../popover-picker/PopoverPicker';
|
||||||
|
|
||||||
@@ -18,9 +19,12 @@ interface SwatchPickerProps {
|
|||||||
export default function SwatchPicker(props: SwatchPickerProps) {
|
export default function SwatchPicker(props: SwatchPickerProps) {
|
||||||
const { color, onChange, isSelected, alwaysDisplayColor } = props;
|
const { color, onChange, isSelected, alwaysDisplayColor } = props;
|
||||||
|
|
||||||
const debouncedOnChange = useDebouncedCallback((newValue: string) => {
|
const debouncedOnChange = useCallback(
|
||||||
onChange(newValue);
|
debounce((newValue: string) => {
|
||||||
}, 100);
|
onChange(newValue);
|
||||||
|
}, 500),
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
const displayColor = alwaysDisplayColor || isSelected ? color : '';
|
const displayColor = alwaysDisplayColor || isSelected ? color : '';
|
||||||
const { color: iconColor } = getAccessibleColour(displayColor);
|
const { color: iconColor } = getAccessibleColour(displayColor);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
border: 2px solid $gray-1200;
|
border: 2px solid $gray-1200;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
$input-font-size: 15px;
|
||||||
|
|
||||||
|
.delayInput {
|
||||||
|
display: flex;
|
||||||
|
gap: $element-spacing;
|
||||||
|
align-items: center;
|
||||||
|
font-size: $text-body-size;
|
||||||
|
|
||||||
|
|
||||||
|
.inputField {
|
||||||
|
font-size: $input-font-size;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
max-width: 7em;
|
||||||
|
padding-left: 16px;
|
||||||
|
color: $ontime-delay-text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delayOptions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
|
||||||
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
|
||||||
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
import Input from '../../../common/components/input/input/Input';
|
import { useEventAction } from '../../../hooks/useEventAction';
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
|
||||||
import BlockRadio from './BlockRadio';
|
|
||||||
|
|
||||||
import style from './DelayInput.module.scss';
|
import style from './DelayInput.module.scss';
|
||||||
|
|
||||||
@@ -12,13 +11,14 @@ interface DelayInputProps {
|
|||||||
duration: number;
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
export default function DelayInput(props: DelayInputProps) {
|
||||||
const { updateEntry } = useEntryActionsContext();
|
const { eventId, duration } = props;
|
||||||
|
const { updateEvent } = useEventAction();
|
||||||
|
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// avoid wrong submit on cancel
|
// avoid wrong submit on cancel
|
||||||
const ignoreChangeRef = useRef(false);
|
let ignoreChange = false;
|
||||||
|
|
||||||
// set internal value on duration change
|
// set internal value on duration change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,8 +33,8 @@ export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
|||||||
* @param {string} newValue string to be parsed
|
* @param {string} newValue string to be parsed
|
||||||
*/
|
*/
|
||||||
const validateAndSubmit = (newValue: string) => {
|
const validateAndSubmit = (newValue: string) => {
|
||||||
if (ignoreChangeRef.current) {
|
if (ignoreChange) {
|
||||||
ignoreChangeRef.current = false;
|
ignoreChange = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitChange = (value: number) => {
|
const submitChange = (value: number) => {
|
||||||
updateEntry({
|
updateEvent({
|
||||||
id: eventId,
|
id: eventId,
|
||||||
duration: value,
|
duration: value,
|
||||||
});
|
});
|
||||||
@@ -76,7 +76,7 @@ export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
|||||||
} else if (event.key === 'Tab') {
|
} else if (event.key === 'Tab') {
|
||||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
ignoreChangeRef.current = true;
|
ignoreChange = true;
|
||||||
setValue(millisToString(duration));
|
setValue(millisToString(duration));
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
}
|
}
|
||||||
@@ -105,10 +105,13 @@ export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={style.delayInput}>
|
<div className={style.delayInput}>
|
||||||
<Input
|
<Input
|
||||||
|
size='sm'
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
data-testid='delay-input'
|
data-testid='delay-input'
|
||||||
className={style.inputField}
|
className={style.inputField}
|
||||||
|
type='text'
|
||||||
placeholder='-'
|
placeholder='-'
|
||||||
|
variant='ontime-filled'
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onBlur={(event) => validateAndSubmit(event.target.value)}
|
onBlur={(event) => validateAndSubmit(event.target.value)}
|
||||||
@@ -116,14 +119,16 @@ export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
|||||||
value={value}
|
value={value}
|
||||||
maxLength={9}
|
maxLength={9}
|
||||||
/>
|
/>
|
||||||
<BlockRadio
|
<RadioGroup
|
||||||
onValueChange={handleSlipChange}
|
className={style.delayOptions}
|
||||||
|
onChange={handleSlipChange}
|
||||||
value={checkedOption}
|
value={checkedOption}
|
||||||
items={[
|
variant='ontime-block'
|
||||||
{ value: 'add', label: 'Add time' },
|
size='sm'
|
||||||
{ value: 'subtract', label: 'Subtract time' },
|
>
|
||||||
]}
|
<Radio value='add'>Add time</Radio>
|
||||||
/>
|
<Radio value='subtract'>Subtract time</Radio>
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
.input {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: inherit;
|
|
||||||
color: $gray-200;
|
|
||||||
border-radius: $component-border-radius-md;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
padding-inline: 0.5em;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $gray-600;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:read-only) {
|
|
||||||
background-color: $gray-1100;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus:not(:read-only) {
|
|
||||||
background-color: $gray-1000;
|
|
||||||
outline: 2px solid $blue-500;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:read-only::placeholder {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtle {
|
|
||||||
background-color: $gray-1200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghosted {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.medium {
|
|
||||||
height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large {
|
|
||||||
height: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fluid {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { InputHTMLAttributes, forwardRef } from 'react';
|
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
|
||||||
|
|
||||||
import style from './Input.module.scss';
|
|
||||||
|
|
||||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
||||||
variant?: 'subtle' | 'ghosted';
|
|
||||||
height?: 'medium' | 'large';
|
|
||||||
fluid?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
||||||
{ className, variant = 'subtle', height = 'medium', fluid, ...inputProps },
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
type='text'
|
|
||||||
autoCorrect='off'
|
|
||||||
autoComplete='off'
|
|
||||||
spellCheck='false'
|
|
||||||
className={cx([style.input, style[variant], style[height], fluid && style.fluid, className])}
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Input;
|
|
||||||
@@ -1,36 +1,20 @@
|
|||||||
// styles from Input.module.scss
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: $gray-200;
|
color: $gray-200;
|
||||||
border-radius: $component-border-radius-md;
|
|
||||||
background-color: $gray-1200;
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
background-color: $gray-1200;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
padding-inline: 0.5em;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover {
|
||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus:not(:read-only) {
|
&:focus {
|
||||||
background-color: $gray-1000;
|
background-color: $gray-1000;
|
||||||
|
color: $gray-50;
|
||||||
border: 1px solid $blue-500;
|
border: 1px solid $blue-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $gray-500;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Popover } from '@base-ui/react/popover';
|
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
|
||||||
import PopoverContents from '../../popover/Popover';
|
|
||||||
|
|
||||||
import style from './PopoverPicker.module.scss';
|
import style from './PopoverPicker.module.scss';
|
||||||
|
|
||||||
@@ -11,14 +9,15 @@ interface PopoverPickerProps {
|
|||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PopoverPicker({ color, onChange, children }: PropsWithChildren<PopoverPickerProps>) {
|
export default function PopoverPicker(props: PropsWithChildren<PopoverPickerProps>) {
|
||||||
|
const { color, onChange, children } = props;
|
||||||
return (
|
return (
|
||||||
<Popover.Root>
|
<Popover>
|
||||||
<Popover.Trigger>{children}</Popover.Trigger>
|
<PopoverTrigger>{children}</PopoverTrigger>
|
||||||
<PopoverContents>
|
<PopoverContent className={style.small} style={{ borderRadius: '9px', width: 'auto' }}>
|
||||||
<HexAlphaColorPicker color={color} onChange={onChange} />
|
<HexAlphaColorPicker color={color} onChange={onChange} />
|
||||||
<HexColorInput color={color} onChange={onChange} className={style.input} prefixed />
|
<HexColorInput color={color} onChange={onChange} className={style.input} prefixed />
|
||||||
</PopoverContents>
|
</PopoverContent>
|
||||||
</Popover.Root>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||