mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cf36f049a | |||
| c6248b0c72 | |||
| 2a890cf2b3 | |||
| a8c611911d | |||
| 8363f06a5c | |||
| 5f040092cb | |||
| 347c748dd9 | |||
| f234a1f892 | |||
| 0c5e87b7e7 | |||
| 64082ceac0 | |||
| f598e8dab1 | |||
| 0c84a3ff9e | |||
| c8760b5e9c | |||
| 34360a5b8f | |||
| 5e6debcce0 | |||
| c191786e50 | |||
| 6911e37c18 | |||
| 86b2132cdc | |||
| 25f0dba83a | |||
| daa032e92c | |||
| ae65ec97d6 | |||
| e7e4302100 | |||
| 7e7d35980a | |||
| 5001dda284 | |||
| a75d35bdc0 | |||
| 2e631194e1 | |||
| f39892dc6e | |||
| 6bcfd87851 | |||
| 4addb50ca4 | |||
| 161ab3fe13 | |||
| 4f4626a163 | |||
| 82d5f02ea0 | |||
| 05aaeddf22 | |||
| 949ab2b7dc | |||
| f12d3ca59c | |||
| 42147d4d5b | |||
| 7850358039 | |||
| 52cae1e22f | |||
| 48f9fb2609 | |||
| ce8a4d230b | |||
| 1553573b94 | |||
| 41a09bf5c3 | |||
| 5dbe7afae8 | |||
| 6633cfc869 | |||
| d127367df9 | |||
| b21ec41ffd | |||
| 66292bc0cf | |||
| b946b245c2 | |||
| c238147b75 | |||
| 5f33c993f6 | |||
| 0e6d6cd416 | |||
| 199dea5df6 | |||
| 5a3ea9f7d0 | |||
| 9ae49a7175 | |||
| d30d9cfd4e | |||
| a0e7b9b665 | |||
| 1dcb7fb08d | |||
| a7e84b9903 | |||
| 3bebbf9ce9 | |||
| ab8da07518 | |||
| c8bae76121 | |||
| 2873698959 | |||
| 741ebef90e | |||
| 0741f46aca | |||
| 5a33ffad92 | |||
| 562f00dc7e | |||
| e50ab0150e | |||
| fe44b33da2 | |||
| 22e392ba9d | |||
| 4997602e9b | |||
| bdb8d4d89a | |||
| b1d5ee12e4 | |||
| a5e733246d | |||
| 1a08b39b8b | |||
| ba1c3235b3 | |||
| 7c1d2f4554 | |||
| 23a44b2f04 | |||
| 2d3c8e7eb2 | |||
| 2f138acad7 | |||
| 176e53dd7f | |||
| 6a31d06c02 | |||
| f739ba122b | |||
| f53cb687bf | |||
| 42dcf35f45 | |||
| 37dc9a620d | |||
| 9e771a2f24 | |||
| b19e9d23ba | |||
| 7e3a9d634b | |||
| 8c14c330f5 | |||
| 3022ec2acd | |||
| cb9528d894 | |||
| 97cb41dc5a | |||
| 8985c8604f | |||
| 2f95aa0c4f | |||
| 5d0e96163f | |||
| 5e98fdb2e9 | |||
| 23298bbe85 | |||
| cc48ac8fd3 | |||
| 2d9909577e | |||
| 3838bdc54d | |||
| c7d8b137ca | |||
| 3a0df0b491 | |||
| 2331205c93 | |||
| 224d3600b2 | |||
| 7d469038bf | |||
| 1c78397f18 | |||
| 73eff20d08 | |||
| 7e3aaa8c30 | |||
| 95536902f5 | |||
| cf206e6220 | |||
| 4662cc8a26 | |||
| 8c52556cc0 | |||
| b1da877dc7 | |||
| d8cef11a25 | |||
| ef5c0a371e | |||
| b2b7855114 | |||
| 129efc33b9 | |||
| 6a40c66a7b | |||
| 2806177c72 | |||
| 4408bdb0d3 | |||
| 871a6b46c8 | |||
| fd39cab845 | |||
| 1cee679397 | |||
| a7a78702b1 | |||
| 26d887c1a6 | |||
| ceb490c60a | |||
| 1849b4d39f | |||
| 9516ac21bb | |||
| 0e173137e8 | |||
| 1202ef3e80 |
@@ -0,0 +1,5 @@
|
|||||||
|
# CodeGraph data files — local to each machine, not for committing.
|
||||||
|
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||||
|
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# Ignore config files
|
# Ignore formatter config files
|
||||||
.prettier*
|
.oxfmt*
|
||||||
|
|
||||||
# Ignore install stuff
|
# Ignore install stuff
|
||||||
**/yarn.lock
|
**/yarn.lock
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
"ONTIME_VERSION.js"
|
|
||||||
dist/
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2020
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"jest": true
|
|
||||||
},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "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
-1
@@ -1,2 +1,2 @@
|
|||||||
github: cpvalente
|
github: cpvalente
|
||||||
buy_me_a_coffee: cpvalente
|
buy_me_a_coffee: cpvalente
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
|
# 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
|
- uses: actions/setup-node@v6
|
||||||
@@ -20,13 +20,13 @@ jobs:
|
|||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ jobs:
|
|||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
@@ -64,4 +64,4 @@ jobs:
|
|||||||
# Push is a shorthand for --output=type=registry
|
# Push is a shorthand for --output=type=registry
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -7,19 +7,23 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_macos:
|
build_macos:
|
||||||
runs-on: macOS-latest
|
runs-on: macOS-26
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
@@ -29,11 +33,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build project packages
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLEID }}
|
APPLE_ID: ${{ secrets.APPLEID }}
|
||||||
@@ -44,8 +43,19 @@ jobs:
|
|||||||
run: pnpm dist-mac --env-mode=loose
|
run: pnpm dist-mac --env-mode=loose
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
- name: Upload macOS build artifacts
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ontime-macos-build
|
||||||
|
path: |
|
||||||
|
./apps/electron/dist/ontime-macOS-x64.dmg
|
||||||
|
./apps/electron/dist/ontime-macOS-arm64.dmg
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
./apps/electron/dist/ontime-macOS-x64.dmg
|
./apps/electron/dist/ontime-macOS-x64.dmg
|
||||||
@@ -57,24 +67,34 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build project packages
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
run: pnpm dist-win
|
run: pnpm dist-win
|
||||||
|
|
||||||
|
- name: Upload Windows build artifacts
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ontime-windows-build
|
||||||
|
path: |
|
||||||
|
./apps/electron/dist/ontime-win64.exe
|
||||||
|
./apps/electron/dist/win-unpacked/
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
with:
|
with:
|
||||||
files: './apps/electron/dist/ontime-win64.exe'
|
files: './apps/electron/dist/ontime-win64.exe'
|
||||||
env:
|
env:
|
||||||
@@ -84,24 +104,35 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build project packages
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
run: pnpm dist-linux
|
run: pnpm dist-linux
|
||||||
|
|
||||||
|
- name: Upload Linux build artifacts
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ontime-linux-build
|
||||||
|
path: |
|
||||||
|
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-arm64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-armv7l.AppImage
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
|
# 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
|
- uses: actions/setup-node@v6
|
||||||
@@ -18,13 +18,13 @@ jobs:
|
|||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
|||||||
+22
-11
@@ -13,16 +13,20 @@ jobs:
|
|||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
@@ -33,6 +37,9 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Run code quality
|
# Run code quality
|
||||||
|
- name: Run formatter
|
||||||
|
run: pnpm format:check
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: pnpm lint
|
||||||
|
|
||||||
@@ -49,16 +56,20 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PNPM_STORE_PATH }}
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
@@ -71,30 +82,30 @@ jobs:
|
|||||||
- name: Get installed Playwright version
|
- name: Get installed Playwright version
|
||||||
run: echo "PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --parseable | cut -s -d '@' -f3 | cut -d '/' -f1)" >> $GITHUB_ENV
|
run: echo "PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --parseable | cut -s -d '@' -f3 | cut -d '/' -f1)" >> $GITHUB_ENV
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/ms-playwright
|
~/.cache/ms-playwright
|
||||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-playwright-
|
${{ runner.os }}-playwright-
|
||||||
- run: npx playwright install --with-deps
|
- run: pnpm playwright install --with-deps
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: pnpm playwright install --with-deps
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: pnpm e2e
|
run: pnpm e2e
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: automated-screenshots
|
name: automated-screenshots
|
||||||
path: automated-screenshots/
|
path: automated-screenshots/
|
||||||
|
|||||||
+6
-1
@@ -33,10 +33,12 @@ e2e/tests/fixtures/tmp/*
|
|||||||
# production
|
# production
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
# bundled assets
|
# bundled assets
|
||||||
translations.json
|
translations.json
|
||||||
override.css
|
override.css
|
||||||
|
**/external/demo/index.html
|
||||||
|
|
||||||
# working stuff
|
# working stuff
|
||||||
**/TODO.md
|
**/TODO.md
|
||||||
@@ -51,4 +53,7 @@ ontime-data/
|
|||||||
**/ONTIME_VERSION.js
|
**/ONTIME_VERSION.js
|
||||||
|
|
||||||
# temporary write files
|
# temporary write files
|
||||||
**.tmp
|
**.tmp
|
||||||
|
|
||||||
|
# Claude Code metadata
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
build
|
|
||||||
coverage
|
|
||||||
dist
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
playwright-report
|
|
||||||
|
|
||||||
|
|
||||||
pnpm-lock.yaml
|
|
||||||
**/*.toml
|
|
||||||
**/*.json
|
|
||||||
!tsconfig.common.json
|
|
||||||
!turbo.json
|
|
||||||
!package.json
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
+31
-22
@@ -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** (~22)
|
||||||
- __pnpm__ (~10)
|
- **pnpm** (~10)
|
||||||
- __docker__ (only necessary to run and build docker images)
|
- **docker** (only necessary to run and build docker images)
|
||||||
|
|
||||||
## LOCAL DEVELOPMENT
|
## LOCAL DEVELOPMENT
|
||||||
|
|
||||||
@@ -20,10 +20,10 @@ development.
|
|||||||
Locally, we would need to run both the React client and the node.js server in development mode
|
Locally, we would need to run both the React client and the node.js server in development mode
|
||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
|
||||||
- __Create a local build__ by running `pnpm build`, this will populate local dependencies
|
|
||||||
- __Run dev mode__ by running `pnpm dev` or `pnpm dev:electron` to get the electron window
|
|
||||||
|
|
||||||
|
- **Install the project dependencies** by running `pnpm i`
|
||||||
|
- **Create a local build** by running `pnpm build`, this will populate local dependencies
|
||||||
|
- **Run dev mode** by running `pnpm dev` or `pnpm dev:electron` to get the electron window
|
||||||
|
|
||||||
### Debugging backend
|
### Debugging backend
|
||||||
|
|
||||||
@@ -31,8 +31,9 @@ The previous command will start the development servers for both the client, ser
|
|||||||
Typically in dev mode we prefer to start these in separate terminals to help with error tracking and debugging.
|
Typically in dev mode we prefer to start these in separate terminals to help with error tracking and debugging.
|
||||||
|
|
||||||
We do that by creating two terminals an running
|
We do that by creating two terminals an running
|
||||||
- __Run the React UI__ by running `pnpm dev --filter=ontime-ui`
|
|
||||||
- __Run the nodejs server__ by running `pnpm dev --filter=ontime-server`
|
- **Run the React UI** by running `pnpm dev --filter=ontime-ui`
|
||||||
|
- **Run the nodejs server** by running `pnpm dev --filter=ontime-server`
|
||||||
|
|
||||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect --filter=ontime-server`.
|
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect --filter=ontime-server`.
|
||||||
|
|
||||||
@@ -77,9 +78,9 @@ 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 `pnpm build`
|
||||||
- __Create the package__ by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
- **Create the package** by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
||||||
|
|
||||||
The build distribution assets will be at `.apps/electron/dist`
|
The build distribution assets will be at `.apps/electron/dist`
|
||||||
|
|
||||||
@@ -90,23 +91,31 @@ 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>`
|
||||||
|
|
||||||
|
## FORMATTING AND LINTING
|
||||||
|
|
||||||
|
Ontime uses [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html) for formatting and [oxlint](https://oxc.rs/docs/guide/usage/linter.html) for linting. Look for an [oxc extension](https://oxc.rs/docs/guide/usage/linter/editors) for your editor of choice.
|
||||||
|
|
||||||
|
- **Run formatter** by running `pnpm format`
|
||||||
|
- **Run linter** by running `pnpm lint`
|
||||||
|
|
||||||
## CONTRIBUTION GUIDELINES
|
## CONTRIBUTION GUIDELINES
|
||||||
|
|
||||||
If you want to propose changes to the codebase, please reach out before opening a Pull Request.
|
If you want to propose changes to the codebase, please reach out before opening a Pull Request.
|
||||||
|
|
||||||
For new PRs, please follow the following checklist:
|
For new PRs, please follow the following checklist:
|
||||||
* [ ] 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
|
- [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
|
||||||
* [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
|
- [ ] You have ran code formatting and linting in all your changes
|
||||||
* [ ] The PR body contains description and motivation for the 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. \
|
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
|
We will review as soon as possible
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
ARG NODE_VERSION=22.15.1
|
ARG NODE_VERSION=22.22.2
|
||||||
FROM node:${NODE_VERSION}-alpine
|
FROM node:${NODE_VERSION}-alpine
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
[](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>
|
||||||
@@ -17,14 +18,17 @@
|
|||||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-armv7l.AppImage">Linux (ARM 32-bit, older Raspberry Pi)</a>
|
- Download 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/@getontime/cli">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.
|
||||||
|
|
||||||
@@ -43,6 +47,7 @@ Ontime is made by entertainment and broadcast engineers and used by
|
|||||||
- Houses of worship
|
- Houses of worship
|
||||||
|
|
||||||
## Main features
|
## Main features
|
||||||
|
|
||||||
- **Multiplatform**: Available as a Cloud service and for Windows, macOS, Linux, or self-hosted via Docker.
|
- **Multiplatform**: Available as a Cloud service and for Windows, macOS, Linux, or self-hosted via Docker.
|
||||||
- **In any device**: Ontime is available 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...
|
||||||
- **Team Collaboration**: Dedicated views for directors, operators, backstage, and signage.
|
- **Team Collaboration**: Dedicated views for directors, operators, backstage, and signage.
|
||||||
@@ -111,7 +116,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)
|
- [Video tutorials](https://www.youtube.com/@ontimeapp)
|
||||||
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
# 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.
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended"
|
|
||||||
],
|
|
||||||
"plugins": [],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-var-requires": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
||||||
|
"extends": ["../../.oxlintrc.json"],
|
||||||
|
"plugins": ["unicorn", "typescript", "oxc", "vitest"]
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
+9
-1
@@ -1,6 +1,7 @@
|
|||||||
# 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.
|
||||||
@@ -10,33 +11,40 @@ 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)
|
||||||
|
|
||||||
|
|||||||
+21
-26
@@ -1,27 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "4.5.0",
|
"version": "4.11.0",
|
||||||
"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:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
build
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
"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",
|
|
||||||
"react-compiler",
|
|
||||||
"simple-import-sort",
|
|
||||||
"@tanstack/query"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react-compiler/react-compiler": "warn",
|
|
||||||
"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)$"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
||||||
|
"extends": ["../../.oxlintrc.json"],
|
||||||
|
"plugins": ["unicorn", "typescript", "oxc", "react", "vitest"]
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
+14
-6
@@ -3,20 +3,28 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<meta name="theme-color" content="#101010" />
|
<meta name="theme-color" content="#101010" />
|
||||||
<meta name="ontime" content="ontime - time keeping for live events" />
|
<meta name="ontime" content="ontime - time keeping for live events" />
|
||||||
<link rel="apple-touch-icon" href="/ontime-logo.png" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<link rel="icon" type="image/png" href="/ontime-logo.png" />
|
<link rel="apple-touch-icon" href="ontime-logo.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="icon" type="image/png" href="ontime-logo.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="site.webmanifest" />
|
||||||
|
<link rel="manifest" href="manifest.json" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<title>ontime</title>
|
<title>ontime</title>
|
||||||
</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>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
if (/\/timer\/?$/.test(window.location.pathname) && typeof Promise.any !== 'function') {
|
||||||
|
var b = document.querySelector('base');
|
||||||
|
var base = b ? (b.getAttribute('href') || '').replace(/\/$/, '') : '';
|
||||||
|
window.location.replace(base + '/timer-legacy' + window.location.search);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+33
-41
@@ -1,44 +1,46 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "4.5.0",
|
"version": "4.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "1.0.0",
|
"@base-ui/react": "1.6.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/lang-css": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource/open-sans": "^5.2.6",
|
"@fontsource/open-sans": "^5.2.7",
|
||||||
"@mantine/hooks": "^8.3.7",
|
"@mantine/hooks": "^8.3.7",
|
||||||
"@sentry/react": "^10.2.0",
|
"@sentry/react": "^10.59.0",
|
||||||
"@table-nav/react": "^0.0.7",
|
"@table-nav/react": "^0.0.7",
|
||||||
"@tanstack/react-query": "^5.85.9",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"@tanstack/react-query-devtools": "^5.85.9",
|
"@tanstack/react-query-devtools": "^5.101.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@uiw/codemirror-theme-vscode": "^4.25.10",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.18.0",
|
||||||
"csv-stringify": "^6.6.0",
|
"csv-stringify": "^6.6.0",
|
||||||
"prismjs": "^1.30.0",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.7",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.7.0",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.7",
|
||||||
"react-fast-compare": "^3.2.2",
|
"react-fast-compare": "^3.2.2",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.80.0",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.6.0",
|
||||||
"react-qr-code": "^2.0.18",
|
"react-router": "^8.0.1",
|
||||||
"react-router": "^7.11.0",
|
"react-virtuoso": "^4.18.7",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"zustand": "^5.0.14"
|
||||||
"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",
|
"dev:electron": "pnpm dev",
|
||||||
"lint": "eslint . --quiet",
|
"lint": "oxlint --quiet --type-aware",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:pipeline": "vitest run",
|
"test:pipeline": "vitest run",
|
||||||
@@ -48,7 +50,8 @@
|
|||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
"not dead",
|
"not dead",
|
||||||
"not op_mini all"
|
"not op_mini all",
|
||||||
|
"safari >= 14.1"
|
||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version",
|
"last 1 chrome version",
|
||||||
@@ -57,32 +60,21 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/vite-plugin": "^2.16.1",
|
"@sentry/vite-plugin": "5.1.1",
|
||||||
"@tanstack/eslint-plugin-query": "^5.8.4",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/prismjs": "^1.26.5",
|
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@vitejs/plugin-legacy": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"@vitejs/plugin-react": "5.2.0",
|
||||||
"@vitejs/plugin-react": "4.5.1",
|
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"eslint": "catalog:",
|
"happy-dom": "^20.9.0",
|
||||||
"eslint-config-prettier": "catalog:",
|
|
||||||
"eslint-plugin-jest": "^28.6.0",
|
|
||||||
"eslint-plugin-prettier": "catalog:",
|
|
||||||
"eslint-plugin-react": "^7.32.0",
|
|
||||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
|
||||||
"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": "8.0.1",
|
||||||
"vite-plugin-compression2": "2.2.0",
|
"vite-plugin-compression2": "2.5.1",
|
||||||
"vite-plugin-svgr": "4.3.0",
|
"vite-plugin-svgr": "4.5.0",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scope": "/",
|
"scope": "./",
|
||||||
"start_url": "/",
|
"start_url": "./",
|
||||||
"display": "",
|
"display": "",
|
||||||
"theme_color": "#121212",
|
"theme_color": "#121212",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#101010"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "",
|
"name": "",
|
||||||
"short_name": "",
|
"short_name": "",
|
||||||
"icons": [
|
"icons": [{ "src": "ontime-logo.png", "sizes": "295x295", "type": "image/png" }],
|
||||||
{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }
|
"theme_color": "#121212",
|
||||||
],
|
|
||||||
"theme_color": "#2B5ABC",
|
|
||||||
"background_color": "#101010",
|
"background_color": "#101010",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { BrowserRouter } from 'react-router';
|
|
||||||
import { Tooltip } from '@base-ui/react/tooltip';
|
import { Tooltip } from '@base-ui/react/tooltip';
|
||||||
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
|
// apply global axios config defaults
|
||||||
import './common/api/axios.config';
|
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 KeepAwake from './features/keep-awake/KeepAwake';
|
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();
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ComponentType, lazy, Suspense, useEffect, useMemo } from 'react';
|
|
||||||
import { Navigate, Route, useLocation, useNavigate, useParams } from 'react-router';
|
|
||||||
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
|
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
|
||||||
|
import { ComponentType, Suspense, lazy, useEffect, useMemo } from 'react';
|
||||||
|
import { Navigate, Route, useLocation, useNavigate, useParams } from 'react-router';
|
||||||
|
|
||||||
import ViewNavigationMenu from './common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from './common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import { PresetContext } from './common/context/PresetContext';
|
import { PresetContext } from './common/context/PresetContext';
|
||||||
import { useClientPath } from './common/hooks/useClientPath';
|
|
||||||
import useUrlPresets from './common/hooks-query/useUrlPresets';
|
import useUrlPresets from './common/hooks-query/useUrlPresets';
|
||||||
|
import { useClientPath } from './common/hooks/useClientPath';
|
||||||
import { getRouteFromPreset } from './common/utils/urlPresets';
|
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 Loader from './views/common/loader/Loader';
|
import Loader from './views/common/loader/Loader';
|
||||||
import NotFound from './views/common/not-found/NotFound';
|
import NotFound from './views/common/not-found/NotFound';
|
||||||
import ViewLoader from './views/ViewLoader';
|
import ViewLoader from './views/ViewLoader';
|
||||||
import { getIsNavigationLocked, sessionScope } from './externals';
|
|
||||||
import { initializeSentry } from './sentry.config';
|
|
||||||
|
|
||||||
const Timer = lazy(() => import('./views/timer/Timer'));
|
const Timer = lazy(() => import('./views/timer/Timer'));
|
||||||
const Countdown = lazy(() => import('./views/countdown/Countdown'));
|
const Countdown = lazy(() => import('./views/countdown/Countdown'));
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import axios from 'axios';
|
|||||||
import { TranslationObject } from 'ontime-types';
|
import { TranslationObject } from 'ontime-types';
|
||||||
|
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
import { TRANSLATION, apiEntryUrl, customTranslationsURL } from './constants';
|
||||||
import { apiEntryUrl, customTranslationsURL, TRANSLATION } from './constants';
|
|
||||||
import type { RequestOptions } from './requestOptions';
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const assetsPath = `${apiEntryUrl}/assets`;
|
const assetsPath = `${apiEntryUrl}/assets`;
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ import { serverURL } from '../../externals';
|
|||||||
// keys in tanstack store
|
// keys in tanstack store
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
|
export const APP_SERVER_PORT = ['appServerPort'];
|
||||||
export const APP_VERSION = ['appVersion'];
|
export const APP_VERSION = ['appVersion'];
|
||||||
export const AUTOMATION = ['automation'];
|
export const AUTOMATION = ['automation'];
|
||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
|
export const CUSTOM_VIEWS = ['customViews'];
|
||||||
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 PROJECT_RUNDOWNS = ['projectRundowns'];
|
||||||
export const RUNDOWN = ['rundown'];
|
export const RUNDOWN = ['rundown'];
|
||||||
|
export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current'];
|
||||||
|
export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
export const CSS_OVERRIDE = ['cssOverride'];
|
||||||
export const CLIENT_LIST = ['clientList'];
|
export const CLIENT_LIST = ['clientList'];
|
||||||
export const REPORT = ['report'];
|
export const REPORT = ['report'];
|
||||||
export const TRANSLATION = ['translation'];
|
export const TRANSLATION = ['translation'];
|
||||||
@@ -21,9 +26,7 @@ export const TRANSLATION = ['translation'];
|
|||||||
export const apiEntryUrl = `${serverURL}/data`;
|
export const apiEntryUrl = `${serverURL}/data`;
|
||||||
|
|
||||||
const userAssetsPath = 'user';
|
const userAssetsPath = 'user';
|
||||||
const cssOverridePath = 'styles/override.css';
|
|
||||||
const customTranslationsPath = 'translations/translations.json';
|
const customTranslationsPath = 'translations/translations.json';
|
||||||
|
|
||||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
|
||||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||||
export const customTranslationsURL = `${serverURL}/${userAssetsPath}/${customTranslationsPath}`;
|
export const customTranslationsURL = `${serverURL}/${userAssetsPath}/${customTranslationsPath}`;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { type CustomViewsListResponse, type MessageResponse } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
import { axiosConfig } from './requestTimeouts';
|
||||||
|
import { downloadBlob } from './utils';
|
||||||
|
|
||||||
|
const customViewsPath = `${apiEntryUrl}/custom-views`;
|
||||||
|
|
||||||
|
export async function getCustomViews(options?: RequestOptions): Promise<CustomViewsListResponse> {
|
||||||
|
const response = await axios.get<CustomViewsListResponse>(customViewsPath, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.shortTimeout,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadCustomView(slug: string, file: File, options?: RequestOptions): Promise<MessageResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('indexHtml', file);
|
||||||
|
|
||||||
|
return (
|
||||||
|
await axios.post<MessageResponse>(`${customViewsPath}/${encodeURIComponent(slug)}/upload`, formData, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadCustomView(slug: string, options?: RequestOptions): Promise<void> {
|
||||||
|
const response = await axios.get(`${customViewsPath}/${encodeURIComponent(slug)}/download`, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadBlob(response.data, `${slug}-index.html`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreDemoView(options?: RequestOptions): Promise<MessageResponse> {
|
||||||
|
return (
|
||||||
|
await axios.post<MessageResponse>(`${customViewsPath}/restore-demo`, null, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCustomView(slug: string, options?: RequestOptions): Promise<void> {
|
||||||
|
await axios.delete(`${customViewsPath}/${encodeURIComponent(slug)}`, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
import type {
|
||||||
|
SpreadsheetPreviewResponse,
|
||||||
|
SpreadsheetWorksheetMetadata,
|
||||||
|
SpreadsheetWorksheetOptions,
|
||||||
|
} from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
@@ -11,12 +15,12 @@ const excelPath = `${apiEntryUrl}/excel`;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* upload Excel file to server
|
* upload Excel file to server
|
||||||
* @return string - file ID op the uploaded file
|
* Uploads an Excel file and returns worksheet names plus metadata for the initial worksheet.
|
||||||
*/
|
*/
|
||||||
export async function upload(file: File, requestOptions?: RequestOptions): Promise<string[]> {
|
export async function upload(file: File, requestOptions?: RequestOptions): Promise<SpreadsheetWorksheetOptions> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('excel', file);
|
formData.append('excel', file);
|
||||||
const response = await axios.post(`${excelPath}/upload`, formData, {
|
const response: AxiosResponse<SpreadsheetWorksheetOptions> = await axios.post(`${excelPath}/upload`, formData, {
|
||||||
signal: requestOptions?.signal,
|
signal: requestOptions?.signal,
|
||||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -26,16 +30,11 @@ export async function upload(file: File, requestOptions?: RequestOptions): Promi
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreviewSpreadsheetResponse = {
|
|
||||||
rundown: Rundown;
|
|
||||||
customFields: CustomFields;
|
|
||||||
summary: RundownSummary;
|
|
||||||
};
|
|
||||||
export async function importRundownPreview(
|
export async function importRundownPreview(
|
||||||
options: ImportMap,
|
options: ImportMap,
|
||||||
requestOptions?: RequestOptions,
|
requestOptions?: RequestOptions,
|
||||||
): Promise<PreviewSpreadsheetResponse> {
|
): Promise<SpreadsheetPreviewResponse> {
|
||||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
|
const response: AxiosResponse<SpreadsheetPreviewResponse> = await axios.post(
|
||||||
`${excelPath}/preview`,
|
`${excelPath}/preview`,
|
||||||
{
|
{
|
||||||
options,
|
options,
|
||||||
@@ -48,6 +47,22 @@ export async function importRundownPreview(
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorksheetMetadata(
|
||||||
|
worksheet: string,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<SpreadsheetWorksheetMetadata> {
|
||||||
|
const response: AxiosResponse<SpreadsheetWorksheetMetadata> = await axios.post(
|
||||||
|
`${excelPath}/metadata`,
|
||||||
|
{ worksheet },
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a xlsx representation of the rundown from the server
|
* Downloads a xlsx representation of the rundown from the server
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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';
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
export type HasUpdate = {
|
export type HasUpdate = {
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ export async function getProjectData(options?: RequestOptions): Promise<ProjectD
|
|||||||
/**
|
/**
|
||||||
* HTTP request to mutate project data
|
* HTTP request to mutate project data
|
||||||
*/
|
*/
|
||||||
export async function postProjectData(data: ProjectData): Promise<AxiosResponse<ProjectData>> {
|
export async function postProjectData(data: ProjectData): Promise<ProjectData> {
|
||||||
return axios.post(projectPath, data);
|
const response = await axios.post(projectPath, data);
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ 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 { apiEntryUrl, REPORT } from './constants';
|
|
||||||
import type { RequestOptions } from './requestOptions';
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
export const reportUrl = `${apiEntryUrl}/report`;
|
export const reportUrl = `${apiEntryUrl}/report`;
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
|
import {
|
||||||
|
EntryId,
|
||||||
|
OntimeEntry,
|
||||||
|
OntimeEvent,
|
||||||
|
ProjectRundownsList,
|
||||||
|
RenumberCues,
|
||||||
|
Rundown,
|
||||||
|
RundownImportPayload,
|
||||||
|
TransientEventPayload,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import type { RequestOptions } from './requestOptions';
|
import type { RequestOptions } from './requestOptions';
|
||||||
@@ -26,19 +35,17 @@ export async function fetchCurrentRundown(options?: RequestOptions): Promise<Run
|
|||||||
throw new Error('Invalid rundown payload');
|
throw new Error('Invalid rundown payload');
|
||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
function isValidRundown(x: any): x is Rundown {
|
/**
|
||||||
return (
|
* HTTP request to fetch all entries in the given rundown
|
||||||
x &&
|
*/
|
||||||
typeof x === 'object' &&
|
export async function fetchRundown(rundownId: RundownId, options?: RequestOptions): Promise<Rundown> {
|
||||||
typeof x.id === 'string' &&
|
const res = await axios.get(`${rundownPath}/${rundownId}`, { signal: options?.signal });
|
||||||
Array.isArray(x.order) &&
|
if (!isValidRundown(res.data)) {
|
||||||
Array.isArray(x.flatOrder) &&
|
throw new Error('Invalid rundown payload');
|
||||||
x.entries &&
|
|
||||||
typeof x.entries === 'object' &&
|
|
||||||
typeof x.revision === 'number'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,6 +83,15 @@ export async function deleteRundown(rundownId: RundownId): Promise<AxiosResponse
|
|||||||
return axios.delete(`${rundownPath}/${rundownId}`);
|
return axios.delete(`${rundownPath}/${rundownId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to apply an imported rundown using a merge strategy or into a new rundown
|
||||||
|
*/
|
||||||
|
export async function importRundownWithOptions(
|
||||||
|
payload: RundownImportPayload,
|
||||||
|
): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.post(`${rundownPath}/import`, payload);
|
||||||
|
}
|
||||||
|
|
||||||
// #endregion operations on project rundowns ======================
|
// #endregion operations on project rundowns ======================
|
||||||
// #region operations on rundown entries ==========================
|
// #region operations on rundown entries ==========================
|
||||||
|
|
||||||
@@ -111,6 +127,13 @@ export async function putBatchEditEvents(rundownId: RundownId, data: BatchEditEn
|
|||||||
return axios.put(`${rundownPath}/${rundownId}/batch`, data);
|
return axios.put(`${rundownPath}/${rundownId}/batch`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to renumber cues for multiple events
|
||||||
|
*/
|
||||||
|
export function patchRenumberCues(rundownId: RundownId, data: RenumberCues): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.patch(`${rundownPath}/${rundownId}/renumber`, data);
|
||||||
|
}
|
||||||
|
|
||||||
export type ReorderEntry = {
|
export type ReorderEntry = {
|
||||||
entryId: EntryId;
|
entryId: EntryId;
|
||||||
destinationId: EntryId;
|
destinationId: EntryId;
|
||||||
@@ -182,3 +205,16 @@ export async function requestDeleteAll(rundownId: RundownId): Promise<AxiosRespo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #endregion operations on rundown entries =======================
|
// #endregion operations on rundown entries =======================
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ export async function postShowWelcomeDialog(show: boolean) {
|
|||||||
/**
|
/**
|
||||||
* HTTP request to retrieve server port
|
* HTTP request to retrieve server port
|
||||||
*/
|
*/
|
||||||
export async function getServerPort(): Promise<PortInfo> {
|
export async function getServerPort(options?: RequestOptions): Promise<PortInfo> {
|
||||||
const res = await axios.get(`${settingsPath}/serverport`);
|
const res = await axios.get(`${settingsPath}/serverport`, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
import type {
|
||||||
|
AuthenticationStatus,
|
||||||
|
SpreadsheetPreviewResponse,
|
||||||
|
SpreadsheetWorksheetMetadata,
|
||||||
|
SpreadsheetWorksheetOptions,
|
||||||
|
} from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
@@ -59,11 +64,7 @@ export const previewRundown = async (
|
|||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ImportMap,
|
options: ImportMap,
|
||||||
requestOptions?: RequestOptions,
|
requestOptions?: RequestOptions,
|
||||||
): Promise<{
|
): Promise<SpreadsheetPreviewResponse> => {
|
||||||
rundown: Rundown;
|
|
||||||
customFields: CustomFields;
|
|
||||||
summary: RundownSummary;
|
|
||||||
}> => {
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${sheetsPath}/${sheetId}/read`,
|
`${sheetsPath}/${sheetId}/read`,
|
||||||
{ options },
|
{ options },
|
||||||
@@ -75,9 +76,34 @@ export const previewRundown = async (
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise<string[]> => {
|
/**
|
||||||
const response: AxiosResponse<string[]> = await axios.post(
|
* Fetches derived metadata for a single worksheet by reading its rows and detecting headers.
|
||||||
`${sheetsPath}/${sheetId}/worksheets`,
|
*/
|
||||||
|
export const getWorksheetMetadata = async (
|
||||||
|
sheetId: string,
|
||||||
|
worksheet: string,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<SpreadsheetWorksheetMetadata> => {
|
||||||
|
const response: AxiosResponse<SpreadsheetWorksheetMetadata> = await axios.post(
|
||||||
|
`${sheetsPath}/${sheetId}/metadata`,
|
||||||
|
{ worksheet },
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the available worksheets for a Google Sheet. Metadata is loaded lazily per worksheet.
|
||||||
|
*/
|
||||||
|
export const getWorksheetOptions = async (
|
||||||
|
sheetId: string,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<SpreadsheetWorksheetOptions> => {
|
||||||
|
const response: AxiosResponse<SpreadsheetWorksheetOptions> = await axios.post(
|
||||||
|
`${sheetsPath}/${sheetId}/worksheet-options`,
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
signal: requestOptions?.signal,
|
signal: requestOptions?.signal,
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
.positioner {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 2px;
|
||||||
|
min-width: var(--anchor-width);
|
||||||
|
max-width: var(--available-width);
|
||||||
|
border: 1px solid $gray-1000;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
background-color: $gray-1200;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&[data-empty][data-hide-empty] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: 20rem;
|
||||||
|
max-height: var(--available-height);
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
scroll-padding-block: 1.5rem;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item,
|
||||||
|
.empty:not(:empty) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
outline: 0;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
&[data-highlighted] {
|
||||||
|
background-color: $blue-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemLabel {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $gray-400;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete';
|
||||||
|
import type { ReactNode, Ref } from 'react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
import type { InputProps } from '../input/input/Input';
|
||||||
|
import { getScrollParent } from './autocompleteInput.utils';
|
||||||
|
|
||||||
|
import inputStyles from '../input/input/Input.module.scss';
|
||||||
|
import styles from './AutocompleteInput.module.scss';
|
||||||
|
|
||||||
|
export interface AutocompleteInputProps extends Omit<InputProps, 'value' | 'defaultValue' | 'onChange'> {
|
||||||
|
options: string[];
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
emptyLabel?: string;
|
||||||
|
trailingElement?: (option: string) => ReactNode;
|
||||||
|
inputRef?: Ref<HTMLInputElement>;
|
||||||
|
openOnFocus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutocompleteInput({
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
emptyLabel,
|
||||||
|
fluid,
|
||||||
|
height = 'medium',
|
||||||
|
inputRef,
|
||||||
|
onValueChange,
|
||||||
|
options,
|
||||||
|
openOnFocus = false,
|
||||||
|
trailingElement,
|
||||||
|
value,
|
||||||
|
variant = 'subtle',
|
||||||
|
...inputProps
|
||||||
|
}: AutocompleteInputProps) {
|
||||||
|
const internalInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { onFocus, ...restInputProps } = inputProps;
|
||||||
|
|
||||||
|
const handleInputRef = (node: HTMLInputElement | null) => {
|
||||||
|
internalInputRef.current = node;
|
||||||
|
|
||||||
|
if (!inputRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof inputRef === 'function') {
|
||||||
|
inputRef(node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRef.current = node;
|
||||||
|
};
|
||||||
|
|
||||||
|
// close the popover when the parent scrollable container scrolls or the window resizes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollTarget = getScrollParent(internalInputRef.current);
|
||||||
|
const handleScroll = () => setOpen(false);
|
||||||
|
|
||||||
|
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
window.addEventListener('resize', handleScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
||||||
|
window.removeEventListener('resize', handleScroll);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAutocomplete.Root
|
||||||
|
items={options}
|
||||||
|
autoHighlight
|
||||||
|
highlightItemOnHover
|
||||||
|
disabled={disabled}
|
||||||
|
open={open}
|
||||||
|
value={value}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
>
|
||||||
|
<BaseAutocomplete.Input
|
||||||
|
ref={handleInputRef}
|
||||||
|
onFocus={(event) => {
|
||||||
|
onFocus?.(event);
|
||||||
|
if (openOnFocus) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cx([
|
||||||
|
inputStyles.input,
|
||||||
|
inputStyles[variant],
|
||||||
|
inputStyles[height],
|
||||||
|
fluid && inputStyles.fluid,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
{...restInputProps}
|
||||||
|
/>
|
||||||
|
<BaseAutocomplete.Portal>
|
||||||
|
<BaseAutocomplete.Positioner side='bottom' align='start' className={styles.positioner}>
|
||||||
|
<BaseAutocomplete.Popup className={styles.popup} data-hide-empty={emptyLabel ? undefined : ''}>
|
||||||
|
<BaseAutocomplete.List className={styles.list}>
|
||||||
|
<BaseAutocomplete.Collection>
|
||||||
|
{(option: string) => (
|
||||||
|
<BaseAutocomplete.Item key={option} value={option} className={styles.item}>
|
||||||
|
<span className={styles.itemLabel}>{option}</span>
|
||||||
|
{trailingElement?.(option)}
|
||||||
|
</BaseAutocomplete.Item>
|
||||||
|
)}
|
||||||
|
</BaseAutocomplete.Collection>
|
||||||
|
{emptyLabel && <BaseAutocomplete.Empty className={styles.empty}>{emptyLabel}</BaseAutocomplete.Empty>}
|
||||||
|
</BaseAutocomplete.List>
|
||||||
|
</BaseAutocomplete.Popup>
|
||||||
|
</BaseAutocomplete.Positioner>
|
||||||
|
</BaseAutocomplete.Portal>
|
||||||
|
</BaseAutocomplete.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export function getScrollParent(node: HTMLElement | null): HTMLElement | Window {
|
||||||
|
let parent = node?.parentElement ?? null;
|
||||||
|
|
||||||
|
while (parent) {
|
||||||
|
const { overflowY } = window.getComputedStyle(parent);
|
||||||
|
if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IoCheckmark } from 'react-icons/io5';
|
|
||||||
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
||||||
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
|
|
||||||
import style from './Checkbox.module.scss';
|
import style from './Checkbox.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useState } from 'react';
|
|||||||
import { IoArrowForward } from 'react-icons/io5';
|
import { IoArrowForward } from 'react-icons/io5';
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
|
||||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||||
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
import Dialog from '../dialog/Dialog';
|
import Dialog from '../dialog/Dialog';
|
||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
|
||||||
|
|
||||||
import IconButton from '../buttons/IconButton';
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { IconType } from 'react-icons';
|
import { IconType } from 'react-icons';
|
||||||
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
|
||||||
|
|
||||||
import style from './DropdownMenu.module.scss';
|
import style from './DropdownMenu.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/* eslint-disable react/destructuring-assignment */
|
|
||||||
import React from 'react';
|
|
||||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
|
|
||||||
import { runtimeStore } from '../../stores/runtime';
|
import { runtimeStore } from '../../stores/runtime';
|
||||||
|
import { getConnectionState, getReconnectAttempts } from '../../utils/socket';
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ class ErrorBoundary extends React.Component {
|
|||||||
scope.setExtras({
|
scope.setExtras({
|
||||||
error,
|
error,
|
||||||
store: appState,
|
store: appState,
|
||||||
hasSocket: { hasConnected, reconnectAttempts },
|
hasSocket: { hasConnected: getConnectionState(), reconnectAttempts: getReconnectAttempts() },
|
||||||
});
|
});
|
||||||
const eventId = Sentry.captureException(error);
|
const eventId = Sentry.captureException(error);
|
||||||
this.setState({ eventId, info });
|
this.setState({ eventId, info });
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -16,6 +16,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $ui-white;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
color: $gray-400;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
svg {
|
svg {
|
||||||
color: $info-blue;
|
color: $info-blue;
|
||||||
|
|||||||
@@ -10,13 +10,37 @@ interface InfoProps {
|
|||||||
type?: 'info' | 'warning' | 'error';
|
type?: 'info' | 'warning' | 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Info({ className, type = 'info', children }: PropsWithChildren<InfoProps>) {
|
interface InfoSectionProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoTitle({ className, children }: PropsWithChildren<InfoSectionProps>) {
|
||||||
|
return <p className={cx([style.title, className])}>{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoBody({ className, children }: PropsWithChildren<InfoSectionProps>) {
|
||||||
|
return <p className={cx([style.body, className])}>{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoFooter({ className, children }: PropsWithChildren<InfoSectionProps>) {
|
||||||
|
return <div className={cx([style.footer, className])}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoRoot({ className, type = 'info', children }: PropsWithChildren<InfoProps>) {
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.infoLabel, style[type], className])}>
|
<div className={cx([style.infoLabel, style[type], className])}>
|
||||||
{type === 'info' && <IoAlertCircle />}
|
{type === 'info' && <IoAlertCircle />}
|
||||||
{type === 'warning' && <IoWarning />}
|
{type === 'warning' && <IoWarning />}
|
||||||
{type === 'error' && <IoWarning />}
|
{type === 'error' && <IoWarning />}
|
||||||
<div>{children}</div>
|
<div className={style.content}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Info = Object.assign(InfoRoot, {
|
||||||
|
Title: InfoTitle,
|
||||||
|
Body: InfoBody,
|
||||||
|
Footer: InfoFooter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Info;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RefObject, useEffect } from 'react';
|
|
||||||
// @ts-expect-error no types from library
|
// @ts-expect-error no types from library
|
||||||
import autosize from 'autosize/dist/autosize';
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useController, UseControllerProps } from 'react-hook-form';
|
|
||||||
import { IoEyedrop } from 'react-icons/io5';
|
|
||||||
import { useDebouncedCallback } from '@mantine/hooks';
|
import { useDebouncedCallback } from '@mantine/hooks';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
import { UseControllerProps, useController } from 'react-hook-form';
|
||||||
|
import { IoEyedrop } from 'react-icons/io5';
|
||||||
|
|
||||||
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
||||||
import PopoverPicker from '../popover-picker/PopoverPicker';
|
import PopoverPicker from '../popover-picker/PopoverPicker';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
import { InputHTMLAttributes, forwardRef } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +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 } from '@base-ui/react/popover';
|
|
||||||
|
|
||||||
import PopoverContents from '../../popover/Popover';
|
import PopoverContents from '../../popover/Popover';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { HotkeyItem, getHotkeyHandler } from '@mantine/hooks';
|
||||||
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks';
|
|
||||||
|
|
||||||
interface UseReactiveTextInputReturn {
|
interface UseReactiveTextInputReturn {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (event: ChangeEvent) => void;
|
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
onBlur: (event: ChangeEvent) => void;
|
onBlur: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@ export default function useReactiveTextInput(
|
|||||||
options?: {
|
options?: {
|
||||||
submitOnEnter?: boolean;
|
submitOnEnter?: boolean;
|
||||||
submitOnCtrlEnter?: boolean;
|
submitOnCtrlEnter?: boolean;
|
||||||
|
submitOnTab?: boolean;
|
||||||
onCancelUpdate?: () => void;
|
onCancelUpdate?: () => void;
|
||||||
|
onTabCancel?: () => void;
|
||||||
allowSubmitSameValue?: boolean;
|
allowSubmitSameValue?: boolean;
|
||||||
allowKeyboardNavigation?: boolean;
|
allowKeyboardNavigation?: boolean;
|
||||||
},
|
},
|
||||||
@@ -23,6 +25,10 @@ export default function useReactiveTextInput(
|
|||||||
const [text, setText] = useState<string>(initialText);
|
const [text, setText] = useState<string>(initialText);
|
||||||
// track whether we are submitting via a submit key (eg enter) and avoid submitting again on blur
|
// track whether we are submitting via a submit key (eg enter) and avoid submitting again on blur
|
||||||
const isKeyboardSubmitting = useRef(false);
|
const isKeyboardSubmitting = useRef(false);
|
||||||
|
// track escape to prevent the subsequent blur from submitting
|
||||||
|
const isEscaping = useRef(false);
|
||||||
|
// track tab-out to prevent blur from submitting (used by timer cells)
|
||||||
|
const isTabbing = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof initialText === 'undefined') {
|
if (typeof initialText === 'undefined') {
|
||||||
@@ -78,6 +84,7 @@ export default function useReactiveTextInput(
|
|||||||
* @param {string} valueToSubmit
|
* @param {string} valueToSubmit
|
||||||
*/
|
*/
|
||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
|
isEscaping.current = true;
|
||||||
// No need to update if it hasn't changed
|
// No need to update if it hasn't changed
|
||||||
setText(initialText);
|
setText(initialText);
|
||||||
// force the text to be the initial value
|
// force the text to be the initial value
|
||||||
@@ -141,16 +148,30 @@ export default function useReactiveTextInput(
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for cells that opt out of tab-submit, track tab-out without preventing navigation
|
||||||
|
if (event.key === 'Tab' && options?.submitOnTab === false) {
|
||||||
|
isTabbing.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
hotKeyHandler(event);
|
hotKeyHandler(event);
|
||||||
};
|
};
|
||||||
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, text]);
|
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, options?.submitOnTab, text]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: text,
|
value: text,
|
||||||
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
onChange: (event) => handleChange(event.target.value),
|
||||||
onBlur: (event: ChangeEvent) => {
|
onBlur: (event) => {
|
||||||
|
if (isTabbing.current) {
|
||||||
|
isTabbing.current = false;
|
||||||
|
(options?.onTabCancel ?? options?.onCancelUpdate)?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isEscaping.current) {
|
||||||
|
isEscaping.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isKeyboardSubmitting.current) {
|
if (!isKeyboardSubmitting.current) {
|
||||||
handleSubmit((event.target as HTMLInputElement).value);
|
handleSubmit(event.target.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyDown: keyHandler,
|
onKeyDown: keyHandler,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { forwardRef, TextareaHTMLAttributes } from 'react';
|
import { TextareaHTMLAttributes, forwardRef } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
import Input from '../input/Input';
|
import Input from '../input/Input';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
import { formatTime, getFormatFromSettings } from '../../../utils/time';
|
||||||
import Input from '../input/Input';
|
import Input from '../input/Input';
|
||||||
|
|
||||||
import style from './TimeInput.module.scss';
|
import style from './TimeInput.module.scss';
|
||||||
@@ -16,6 +17,7 @@ interface TimeInputProps<T extends string> {
|
|||||||
align?: 'left' | 'center';
|
align?: 'left' | 'center';
|
||||||
delayed?: boolean;
|
delayed?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
shouldFormat?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TimeInput<T extends string>({
|
export default function TimeInput<T extends string>({
|
||||||
@@ -28,10 +30,12 @@ export default function TimeInput<T extends string>({
|
|||||||
align = 'center',
|
align = 'center',
|
||||||
delayed,
|
delayed,
|
||||||
className,
|
className,
|
||||||
|
shouldFormat,
|
||||||
}: TimeInputProps<T>) {
|
}: TimeInputProps<T>) {
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
const ignoreChange = useRef(false);
|
const ignoreChange = useRef(false);
|
||||||
|
const formatAs12Hr = useMemo(() => shouldFormat && getFormatFromSettings() === '12', [shouldFormat]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Resets input value to given
|
* @description Resets input value to given
|
||||||
@@ -39,10 +43,12 @@ export default function TimeInput<T extends string>({
|
|||||||
const resetValue = useCallback(() => {
|
const resetValue = useCallback(() => {
|
||||||
if (typeof time !== 'number' || isNaN(time)) {
|
if (typeof time !== 'number' || isNaN(time)) {
|
||||||
setValue('00:00:00');
|
setValue('00:00:00');
|
||||||
|
} else if (shouldFormat) {
|
||||||
|
setValue(formatTime(time));
|
||||||
} else {
|
} else {
|
||||||
setValue(millisToString(time));
|
setValue(millisToString(time));
|
||||||
}
|
}
|
||||||
}, [time]);
|
}, [time, shouldFormat]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Selects input text on focus
|
* @description Selects input text on focus
|
||||||
@@ -139,9 +145,10 @@ export default function TimeInput<T extends string>({
|
|||||||
onBlur={onBlurHandler}
|
onBlur={onBlurHandler}
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
value={value}
|
value={value}
|
||||||
maxLength={8}
|
maxLength={formatAs12Hr ? 11 : 8}
|
||||||
style={{
|
style={{
|
||||||
textAlign: align,
|
textAlign: align,
|
||||||
|
width: formatAs12Hr ? '7.5em' : '6.5em',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.kbd {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
padding: 0.0625rem 0.375rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: $gray-1100;
|
||||||
|
color: $ui-white;
|
||||||
|
box-shadow: 0 0 3px rgba(0, 0, 0, 0.4);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: calc(1rem - 4px);
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import style from './Kbd.module.scss';
|
||||||
|
|
||||||
|
export default function Kbd({ children }: PropsWithChildren) {
|
||||||
|
return <kbd className={style.kbd}>{children}</kbd>;
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 1px 0 0 currentColor;
|
box-shadow: 0 1px 0 0 currentColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
top: 10vh;
|
top: 10vh;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
min-width: min(880px, 90vw);
|
min-width: min(880px, 90vw);
|
||||||
min-height: min(200px, 10vh);
|
min-height: min(200px, 10vh);
|
||||||
@@ -11,12 +11,22 @@
|
|||||||
|
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
border-radius: 3px;
|
border-radius: $component-border-radius-md;
|
||||||
box-shadow: $box-shadow-l1;
|
box-shadow: $box-shadow-l1;
|
||||||
border: 1px solid $gray-1100;
|
border: 1px solid $gray-1100;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
top: 4vh;
|
||||||
|
min-width: min(1280px, 96vw);
|
||||||
|
max-width: min(1800px, 98vw);
|
||||||
|
height: 88vh;
|
||||||
|
max-height: 88vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -30,8 +40,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding-block: 1rem;
|
padding-block: 1rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -39,6 +47,11 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titleText {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -48,6 +61,13 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide .body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
padding-block: 1rem;
|
padding-block: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
import IconButton from '../buttons/IconButton';
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
import style from './Modal.module.scss';
|
import style from './Modal.module.scss';
|
||||||
@@ -11,6 +12,7 @@ interface ModalProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
showBackdrop?: boolean;
|
showBackdrop?: boolean;
|
||||||
|
size?: 'default' | 'wide';
|
||||||
bodyElements: ReactNode;
|
bodyElements: ReactNode;
|
||||||
footerElements?: ReactNode;
|
footerElements?: ReactNode;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -21,6 +23,7 @@ export default function Modal({
|
|||||||
title,
|
title,
|
||||||
showCloseButton,
|
showCloseButton,
|
||||||
showBackdrop,
|
showBackdrop,
|
||||||
|
size = 'default',
|
||||||
bodyElements,
|
bodyElements,
|
||||||
footerElements,
|
footerElements,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -35,9 +38,9 @@ export default function Modal({
|
|||||||
>
|
>
|
||||||
<BaseDialog.Portal>
|
<BaseDialog.Portal>
|
||||||
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
||||||
<BaseDialog.Popup className={style.modal}>
|
<BaseDialog.Popup aria-label={title} className={cx([style.modal, size === 'wide' && style.wide])}>
|
||||||
<div className={style.title}>
|
<div className={style.title}>
|
||||||
{title}
|
{title ? <BaseDialog.Title className={style.titleText}>{title}</BaseDialog.Title> : <div />}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<IconButton variant='subtle-white' onClick={onClose}>
|
<IconButton variant='subtle-white' onClick={onClose}>
|
||||||
<IoClose />
|
<IoClose />
|
||||||
@@ -45,7 +48,7 @@ export default function Modal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={style.body}>{bodyElements}</div>
|
<div className={style.body}>{bodyElements}</div>
|
||||||
<div className={style.footer}>{footerElements}</div>
|
{footerElements ? <div className={style.footer}>{footerElements}</div> : null}
|
||||||
</BaseDialog.Popup>
|
</BaseDialog.Popup>
|
||||||
</BaseDialog.Portal>
|
</BaseDialog.Portal>
|
||||||
</BaseDialog.Root>
|
</BaseDialog.Root>
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ $progress-bar-br: 3px;
|
|||||||
.multiprogress-bar__indicator-bar {
|
.multiprogress-bar__indicator-bar {
|
||||||
background-color: var(--background-color-override, $ui-black);
|
background-color: var(--background-color-override, $ui-black);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: 1s linear;
|
|
||||||
transition-property: width;
|
|
||||||
|
|
||||||
.multiprogress-bar--ignore-css-override & {
|
.multiprogress-bar--ignore-css-override & {
|
||||||
background-color: $ui-black;
|
background-color: $ui-black;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useAnimatedProgress } from '../../hooks/useAnimatedProgress';
|
||||||
import { getProgress } from '../../utils/getProgress';
|
import { getProgress } from '../../utils/getProgress';
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
|||||||
className = '',
|
className = '',
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const percentRemaining = 100 - getProgress(now, complete);
|
const percentRemaining = 100 - useAnimatedProgress(now, complete);
|
||||||
const dangerWidth = danger ? 100 - getProgress(danger, complete) : 0;
|
const dangerWidth = danger ? 100 - getProgress(danger, complete) : 0;
|
||||||
const warningWidth = warning ? 100 - dangerWidth - getProgress(warning, complete) : 0;
|
const warningWidth = warning ? 100 - dangerWidth - getProgress(warning, complete) : 0;
|
||||||
const isOvertime = now !== null && now < 0;
|
const isOvertime = now !== null && now < 0;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
import { Dialog } from '@base-ui/react/dialog';
|
||||||
|
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||||
import { LuCoffee } from 'react-icons/lu';
|
import { LuCoffee } from 'react-icons/lu';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { Dialog } from '@base-ui/react/dialog';
|
|
||||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
|
||||||
|
|
||||||
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
||||||
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
|
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
import IconButton from '../buttons/IconButton';
|
import IconButton from '../buttons/IconButton';
|
||||||
import { RenameClientModal } from '../client-modal/RenameClientModal';
|
import { RenameClientModal } from '../client-modal/RenameClientModal';
|
||||||
|
|
||||||
import ClientLink from './client-link/ClientLink';
|
import ClientLink from './client-link/ClientLink';
|
||||||
import EditorNavigation from './editor-navigation/EditorNavigation';
|
import EditorNavigation from './editor-navigation/EditorNavigation';
|
||||||
import NavigationMenuItem from './navigation-menu-item/NavigationMenuItem';
|
import NavigationMenuItem from './navigation-menu-item/NavigationMenuItem';
|
||||||
@@ -106,6 +106,8 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
|||||||
{route.label}
|
{route.label}
|
||||||
</ClientLink>
|
</ClientLink>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<PresetNavigation isSmallScreen={isSmallScreen} onClose={onClose} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLocalhost && (
|
{isLocalhost && (
|
||||||
@@ -118,3 +120,27 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
|||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PresetNavigation({ isSmallScreen, onClose }: { isSmallScreen: boolean; onClose: () => void }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const { data: urlPresets } = useUrlPresets();
|
||||||
|
const navPresets = urlPresets.filter((preset) => preset.enabled && preset.displayInNav);
|
||||||
|
|
||||||
|
if (navPresets.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr className={style.separator} />
|
||||||
|
{navPresets.map((preset) => (
|
||||||
|
<ClientLink
|
||||||
|
key={preset.alias}
|
||||||
|
to={`preset/${preset.alias}`}
|
||||||
|
current={location.pathname === `/preset/${preset.alias}`}
|
||||||
|
postAction={isSmallScreen ? onClose : undefined}
|
||||||
|
>
|
||||||
|
{preset.alias}
|
||||||
|
</ClientLink>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { hasCustomParams, useSavedViewParams } from '../../stores/savedViewParams';
|
||||||
import { useViewParamsEditorStore } from '../view-params-editor/viewParamsEditor.store';
|
import { useViewParamsEditorStore } from '../view-params-editor/viewParamsEditor.store';
|
||||||
|
|
||||||
import FloatingNavigation from './floating-navigation/FloatingNavigation';
|
import FloatingNavigation from './floating-navigation/FloatingNavigation';
|
||||||
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
|
|
||||||
import NavigationMenu from './NavigationMenu';
|
import NavigationMenu from './NavigationMenu';
|
||||||
|
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
|
||||||
|
|
||||||
interface ViewNavigationMenuProps {
|
interface ViewNavigationMenuProps {
|
||||||
/** prevent navigation */
|
/** prevent navigation */
|
||||||
@@ -18,6 +19,9 @@ export default memo(ViewNavigationMenu);
|
|||||||
function ViewNavigationMenu({ isNavigationLocked, suppressSettings }: ViewNavigationMenuProps) {
|
function ViewNavigationMenu({ isNavigationLocked, suppressSettings }: ViewNavigationMenuProps) {
|
||||||
const [isMenuOpen, menuHandler] = useDisclosure();
|
const [isMenuOpen, menuHandler] = useDisclosure();
|
||||||
const { open: showEditFormDrawer } = useViewParamsEditorStore();
|
const { open: showEditFormDrawer } = useViewParamsEditorStore();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const savedParams = useSavedViewParams((store) => store.params);
|
||||||
|
const hasSavedChanges = hasCustomParams(searchParams) || Object.keys(savedParams).length > 0;
|
||||||
|
|
||||||
useHotkeys([
|
useHotkeys([
|
||||||
[
|
[
|
||||||
@@ -47,6 +51,7 @@ function ViewNavigationMenu({ isNavigationLocked, suppressSettings }: ViewNaviga
|
|||||||
<FloatingNavigation
|
<FloatingNavigation
|
||||||
toggleMenu={isNavigationLocked ? undefined : menuHandler.toggle}
|
toggleMenu={isNavigationLocked ? undefined : menuHandler.toggle}
|
||||||
toggleSettings={suppressSettings ? undefined : showEditFormDrawer}
|
toggleSettings={suppressSettings ? undefined : showEditFormDrawer}
|
||||||
|
hasSavedChanges={hasSavedChanges}
|
||||||
/>
|
/>
|
||||||
{!isNavigationLocked && <NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />}
|
{!isNavigationLocked && <NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+26
-1
@@ -1,4 +1,29 @@
|
|||||||
.linkIcon {
|
.trailing {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkIcon {
|
||||||
@include rotate-fourty-five;
|
@include rotate-fourty-five;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $active-indicator;
|
||||||
|
pointer-events: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity $transition-time-action;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { MouseEvent, PropsWithChildren } from 'react';
|
||||||
import { IoArrowUp } from 'react-icons/io5';
|
import { IoArrowUp, IoCloseOutline } from 'react-icons/io5';
|
||||||
import { useNavigate } from 'react-router';
|
import { useLocation, useNavigate, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { useElectronEvent } from '../../../hooks/useElectronEvent';
|
import { useElectronEvent } from '../../../hooks/useElectronEvent';
|
||||||
|
import {
|
||||||
|
hasCustomParams,
|
||||||
|
reservedParams,
|
||||||
|
stripReservedParams,
|
||||||
|
useSavedViewParams,
|
||||||
|
} from '../../../stores/savedViewParams';
|
||||||
import { handleLinks } from '../../../utils/linkUtils';
|
import { handleLinks } from '../../../utils/linkUtils';
|
||||||
|
import IconButton from '../../buttons/IconButton';
|
||||||
|
import Tooltip from '../../tooltip/Tooltip';
|
||||||
import NavigationMenuItem from '../navigation-menu-item/NavigationMenuItem';
|
import NavigationMenuItem from '../navigation-menu-item/NavigationMenuItem';
|
||||||
|
|
||||||
import style from './ClientLink.module.scss';
|
import style from './ClientLink.module.scss';
|
||||||
@@ -16,32 +24,104 @@ interface ClientLinkProps {
|
|||||||
|
|
||||||
export default function ClientLink({ current, to, postAction, children }: PropsWithChildren<ClientLinkProps>) {
|
export default function ClientLink({ current, to, postAction, children }: PropsWithChildren<ClientLinkProps>) {
|
||||||
const { isElectron } = useElectronEvent();
|
const { isElectron } = useElectronEvent();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuItem
|
<ElectronNavigationItem current={current} to={to} postAction={postAction}>
|
||||||
active={current}
|
|
||||||
onClick={() => {
|
|
||||||
handleLinks(to);
|
|
||||||
postAction?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
<IoArrowUp className={style.linkIcon} />
|
</ElectronNavigationItem>
|
||||||
</NavigationMenuItem>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationMenuItem
|
<BrowserNavigationItem current={current} to={to} postAction={postAction}>
|
||||||
active={current}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/${to}`);
|
|
||||||
postAction?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
|
</BrowserNavigationItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ElectronNavigationItem({ current, to, postAction, children }: PropsWithChildren<ClientLinkProps>) {
|
||||||
|
const navigateToLink = () => {
|
||||||
|
handleLinks(to);
|
||||||
|
postAction?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationMenuItem active={current} onClick={navigateToLink}>
|
||||||
|
{children}
|
||||||
|
<span className={style.trailing}>
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</span>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BrowserNavigationItem({ current, to, postAction, children }: PropsWithChildren<ClientLinkProps>) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const { params: savedParams, save, clear } = useSavedViewParams();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the params of the view we are leaving and resolve the destination,
|
||||||
|
* restoring any params previously saved for the target view.
|
||||||
|
*/
|
||||||
|
const resolveDestination = () => {
|
||||||
|
const currentView = location.pathname.replace(/^\//, '');
|
||||||
|
save(currentView, stripReservedParams(location.search));
|
||||||
|
const restored = savedParams[to];
|
||||||
|
return `${to}${restored ? `?${restored}` : ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCustomised = current ? hasCustomParams(searchParams) : Boolean(savedParams[to]);
|
||||||
|
|
||||||
|
const navigateToLink = () => {
|
||||||
|
const destination = resolveDestination();
|
||||||
|
navigate(`/${destination}`);
|
||||||
|
postAction?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the saved settings for this route without navigating to it.
|
||||||
|
* When it is the current view, also reset the live URL to its defaults.
|
||||||
|
*/
|
||||||
|
const clearViewSettings = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
clear(to);
|
||||||
|
|
||||||
|
// if mounted, clear the URL params
|
||||||
|
if (current) {
|
||||||
|
const preserved = new URLSearchParams();
|
||||||
|
reservedParams.forEach((key) => {
|
||||||
|
const value = searchParams.get(key);
|
||||||
|
if (value !== null) preserved.set(key, value);
|
||||||
|
});
|
||||||
|
setSearchParams(preserved);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationMenuItem active={current} onClick={navigateToLink}>
|
||||||
|
{children}
|
||||||
|
{isCustomised && (
|
||||||
|
<span className={style.trailing}>
|
||||||
|
<span className={style.indicator} aria-hidden data-testid='client-link__saved-indicator' />
|
||||||
|
<Tooltip
|
||||||
|
text='Reset to default'
|
||||||
|
render={
|
||||||
|
<IconButton
|
||||||
|
variant='ghosted-white'
|
||||||
|
size='small'
|
||||||
|
className={style.clear}
|
||||||
|
aria-label='Reset to default'
|
||||||
|
onClick={clearViewSettings}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IoCloseOutline />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -22,3 +22,19 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: $zindex-nav;
|
z-index: $zindex-nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonWithIndicator {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.15rem;
|
||||||
|
right: -0.15rem;
|
||||||
|
width: 0.6rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $active-indicator;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|||||||
+14
-10
@@ -9,9 +9,10 @@ import style from './FloatingNavigation.module.scss';
|
|||||||
interface FloatingNavigationProps {
|
interface FloatingNavigationProps {
|
||||||
toggleMenu?: () => void;
|
toggleMenu?: () => void;
|
||||||
toggleSettings?: () => void;
|
toggleSettings?: () => void;
|
||||||
|
hasSavedChanges?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FloatingNavigation({ toggleMenu, toggleSettings }: FloatingNavigationProps) {
|
export default function FloatingNavigation({ toggleMenu, toggleSettings, hasSavedChanges }: FloatingNavigationProps) {
|
||||||
const isButtonShown = useFadeOutOnInactivity(true);
|
const isButtonShown = useFadeOutOnInactivity(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,15 +21,18 @@ export default function FloatingNavigation({ toggleMenu, toggleSettings }: Float
|
|||||||
className={cx([style.fadeable, style.buttonContainer, !isButtonShown && style.hidden])}
|
className={cx([style.fadeable, style.buttonContainer, !isButtonShown && style.hidden])}
|
||||||
>
|
>
|
||||||
{toggleMenu && (
|
{toggleMenu && (
|
||||||
<IconButton
|
<div className={style.buttonWithIndicator}>
|
||||||
variant='subtle-white'
|
<IconButton
|
||||||
size='xlarge'
|
variant='subtle-white'
|
||||||
onClick={toggleMenu}
|
size='xlarge'
|
||||||
aria-label='toggle menu'
|
onClick={toggleMenu}
|
||||||
data-testid='navigation__toggle-menu'
|
aria-label='toggle menu'
|
||||||
>
|
data-testid='navigation__toggle-menu'
|
||||||
<IoApps />
|
>
|
||||||
</IconButton>
|
<IoApps />
|
||||||
|
</IconButton>
|
||||||
|
{hasSavedChanges && <span className={style.indicator} data-testid='navigation__saved-indicator' />}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{toggleSettings && (
|
{toggleSettings && (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
+3
-1
@@ -24,7 +24,9 @@ export default function NavigationMenuItem({
|
|||||||
role='button'
|
role='button'
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
isKeyEnter(event) && onClick();
|
if (isKeyEnter(event)) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: $box-shadow-l1;
|
box-shadow: $box-shadow-l1;
|
||||||
border: 1px solid $gray-1000;
|
border: 1px solid $gray-1000;
|
||||||
|
|
||||||
@@ -27,4 +27,3 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
|
||||||
import { Popover } from '@base-ui/react/popover';
|
import { Popover } from '@base-ui/react/popover';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import style from './Popover.module.scss';
|
import style from './Popover.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,4 @@ $progress-bar-br: 3px;
|
|||||||
.progress-bar__indicator {
|
.progress-bar__indicator {
|
||||||
height: $progress-bar-size;
|
height: $progress-bar-size;
|
||||||
background-color: var(--timer-progress-override, $accent-color);
|
background-color: var(--timer-progress-override, $accent-color);
|
||||||
transition: 1s linear;
|
|
||||||
transition-property: width;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
|
||||||
import { getProgress } from '../../utils/getProgress';
|
import { useAnimatedProgress } from '../../hooks/useAnimatedProgress';
|
||||||
|
|
||||||
import './ProgressBar.scss';
|
import './ProgressBar.scss';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ interface ProgressBarProps {
|
|||||||
|
|
||||||
export default function ProgressBar(props: ProgressBarProps) {
|
export default function ProgressBar(props: ProgressBarProps) {
|
||||||
const { current, duration, className } = props;
|
const { current, duration, className } = props;
|
||||||
const progress = getProgress(current, duration);
|
const progress = useAnimatedProgress(current, duration);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`progress-bar__bg ${className}`}>
|
<div className={`progress-bar__bg ${className}`}>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { PropsWithChildren, useCallback, useContext } from 'react';
|
|||||||
|
|
||||||
import { isLocalhost } from '../../../externals';
|
import { isLocalhost } from '../../../externals';
|
||||||
import { AppContext } from '../../context/AppContext';
|
import { AppContext } from '../../context/AppContext';
|
||||||
|
|
||||||
import PinPage from './PinPage';
|
import PinPage from './PinPage';
|
||||||
|
|
||||||
interface ProtectRouteProps {
|
interface ProtectRouteProps {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
.square {
|
||||||
|
border-radius: $component-border-radius-sm;
|
||||||
|
background-color: $viewer-color;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blink {
|
||||||
|
animation: blink 1s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0% {
|
||||||
|
opacity: 10%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { toString } from 'qrcode';
|
||||||
|
import { Suspense, use } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './QrCode.module.scss';
|
||||||
|
|
||||||
|
interface QRCodeProps {
|
||||||
|
value: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QRCode({ value, size }: QRCodeProps) {
|
||||||
|
'use memo';
|
||||||
|
const svgPromise = toString(value, {
|
||||||
|
width: size,
|
||||||
|
margin: 2,
|
||||||
|
color: { dark: '#101010', light: '#cfcfcf' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<QrFallback size={size} />}>
|
||||||
|
<QrSvg svgPromise={svgPromise} size={size} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QrFallback({ size }: { size: number }) {
|
||||||
|
'use memo';
|
||||||
|
return <span style={{ width: size + 2, height: size + 2 }} className={cx([style.blink, style.square])} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QrSvg({ svgPromise, size }: { svgPromise: Promise<string>; size: number }) {
|
||||||
|
'use memo';
|
||||||
|
const svg = use(svgPromise);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={style.square}
|
||||||
|
style={{ width: size + 2, height: size + 2 }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: svg }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ $thumb-color-hover: $white-60;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 2px solid $blue-500;
|
outline: 2px solid $blue-500;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CSSProperties, PropsWithChildren, Ref } from 'react';
|
|
||||||
import { ScrollArea } from '@base-ui/react/scroll-area';
|
import { ScrollArea } from '@base-ui/react/scroll-area';
|
||||||
|
import { CSSProperties, PropsWithChildren, Ref } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,11 @@
|
|||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&[data-visible] {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { Select as BaseSelect } from '@base-ui/react/select';
|
||||||
import { IoCheckmark } from 'react-icons/io5';
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
import { LuChevronsUpDown } from 'react-icons/lu';
|
import { LuChevronsUpDown } from 'react-icons/lu';
|
||||||
import { Select as BaseSelect } from '@base-ui/react/select';
|
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ export default function Select<T>({ options, fluid, size = 'medium', ...selectRo
|
|||||||
</BaseSelect.Trigger>
|
</BaseSelect.Trigger>
|
||||||
<BaseSelect.Portal>
|
<BaseSelect.Portal>
|
||||||
<BaseSelect.Positioner side='bottom' align='start'>
|
<BaseSelect.Positioner side='bottom' align='start'>
|
||||||
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} keepMounted />
|
||||||
<BaseSelect.Popup className={styles.popup}>
|
<BaseSelect.Popup className={styles.popup}>
|
||||||
<BaseSelect.Arrow />
|
<BaseSelect.Arrow />
|
||||||
<BaseSelect.List className={styles.list}>
|
<BaseSelect.List className={styles.list}>
|
||||||
@@ -43,7 +43,7 @@ export default function Select<T>({ options, fluid, size = 'medium', ...selectRo
|
|||||||
))}
|
))}
|
||||||
</BaseSelect.List>
|
</BaseSelect.List>
|
||||||
</BaseSelect.Popup>
|
</BaseSelect.Popup>
|
||||||
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} keepMounted />
|
||||||
</BaseSelect.Positioner>
|
</BaseSelect.Positioner>
|
||||||
</BaseSelect.Portal>
|
</BaseSelect.Portal>
|
||||||
</BaseSelect.Root>
|
</BaseSelect.Root>
|
||||||
|
|||||||
@@ -4,15 +4,17 @@
|
|||||||
color: $white-10;
|
color: $white-10;
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
width: 100%;
|
display: block;
|
||||||
|
width: min(100%, 24rem);
|
||||||
|
margin-inline: auto;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: block;
|
display: block;
|
||||||
margin-inline: auto;;
|
margin-inline: auto;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
max-width: 600px;
|
max-width: min(100%, 600px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
padding-top: 10vh;
|
padding-top: 10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.inline {
|
||||||
width: 100%;
|
display: flex;
|
||||||
opacity: 0.8;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
import EmptyImage from '../../../assets/images/empty.svg?react';
|
import { SupportedEntry } from 'ontime-types';
|
||||||
|
import { IoAdd } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import Empty from './Empty';
|
||||||
|
|
||||||
import style from './EmptyTableBody.module.scss';
|
import style from './EmptyTableBody.module.scss';
|
||||||
|
|
||||||
interface EmptyTableBodyProps {
|
interface EmptyTableBodyProps {
|
||||||
text: string;
|
handleAddNew?: (type: SupportedEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmptyTableBody({ text }: EmptyTableBodyProps) {
|
export default function EmptyTableBody({ handleAddNew }: EmptyTableBodyProps) {
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
|
const text = getLocalizedString('common.no_data');
|
||||||
return (
|
return (
|
||||||
<tbody className={style.emptyContainer}>
|
<tbody className={style.emptyContainer}>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={99} className={style.emptyCell}>
|
<td colSpan={99} className={style.emptyCell}>
|
||||||
<EmptyImage className={style.empty} />
|
<Empty injectedStyles={{ marginTop: '5vh' }} />
|
||||||
{text && <span className={style.text}>{text}</span>}
|
<span className={style.text}>{text}</span>
|
||||||
|
{handleAddNew && (
|
||||||
|
<div className={style.inline}>
|
||||||
|
<Button onClick={() => handleAddNew(SupportedEntry.Event)} variant='primary' size='large'>
|
||||||
|
<IoAdd />
|
||||||
|
Create Event
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
/* Reset and base styles */
|
/* Reset and base styles */
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
transition: background-color 125ms cubic-bezier(0.26, 0.75, 0.38, 0.45);
|
transition: background-color 125ms cubic-bezier(0.26, 0.75, 0.38, 0.45);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
.tag {
|
.tag {
|
||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
background-color: $gray-900;
|
|
||||||
color: $ui-white;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.default {
|
||||||
|
background-color: $gray-900;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background-color: $orange-1300;
|
||||||
|
color: $orange-300;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './Tag.module.scss';
|
import style from './Tag.module.scss';
|
||||||
|
|
||||||
interface TagProps {
|
interface TagProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: 'default' | 'warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tag({ className, children }: PropsWithChildren<TagProps>) {
|
export default function Tag({ className, variant = 'default', children }: PropsWithChildren<TagProps>) {
|
||||||
return <span className={`${style.tag} ${className || ''}`}>{children}</span>;
|
return <span className={cx([style.tag, style[variant], className])}>{children}</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
background-color: $ui-white;
|
background-color: $ui-white;
|
||||||
color: $ui-black;
|
color: $ui-black;
|
||||||
padding: 0.125rem 0.5rem;
|
padding: 0.125rem 0.5rem;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
line-height: 1.25em;
|
line-height: 1.25em;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
|
||||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import style from './Tooltip.module.scss';
|
import style from './Tooltip.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import Checkbox from '../checkbox/Checkbox';
|
|||||||
import Input from '../input/input/Input';
|
import Input from '../input/input/Input';
|
||||||
import Select, { SelectOption } from '../select/Select';
|
import Select, { SelectOption } from '../select/Select';
|
||||||
import Switch from '../switch/Switch';
|
import Switch from '../switch/Switch';
|
||||||
|
|
||||||
import InlineColourPicker from './InlineColourPicker';
|
import InlineColourPicker from './InlineColourPicker';
|
||||||
import { ParamField } from './viewParams.types';
|
import { ParamField } from './viewParams.types';
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ export default function ParamInput({ paramField }: ParamInputProps) {
|
|||||||
return (
|
return (
|
||||||
<MultiOption
|
<MultiOption
|
||||||
paramField={paramField}
|
paramField={paramField}
|
||||||
options={optionFromParams.length ? optionFromParams : paramField.defaultValue ?? ['']}
|
options={optionFromParams.length ? optionFromParams : (paramField.defaultValue ?? [''])}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import { Dialog } from '@base-ui/react/dialog';
|
||||||
|
import { OntimeView } from 'ontime-types';
|
||||||
import { FormEvent, memo } from 'react';
|
import { FormEvent, memo } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
import { Dialog } from '@base-ui/react/dialog';
|
|
||||||
import { OntimeView } from 'ontime-types';
|
|
||||||
|
|
||||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
|
||||||
import useViewSettings from '../../hooks-query/useViewSettings';
|
import useViewSettings from '../../hooks-query/useViewSettings';
|
||||||
|
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||||
|
import { useSavedViewParams } from '../../stores/savedViewParams';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
import IconButton from '../buttons/IconButton';
|
import IconButton from '../buttons/IconButton';
|
||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
|
|
||||||
import { ViewOption } from './viewParams.types';
|
import { ViewOption } from './viewParams.types';
|
||||||
import { getPreservedSearchParams, getURLSearchParamsFromObj } from './viewParams.utils';
|
import { getPreservedSearchParams, getURLSearchParamsFromObj } from './viewParams.utils';
|
||||||
import { useViewParamsEditorStore } from './viewParamsEditor.store';
|
import { useViewParamsEditorStore } from './viewParamsEditor.store';
|
||||||
@@ -28,6 +28,7 @@ function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { data: viewSettings } = useViewSettings();
|
const { data: viewSettings } = useViewSettings();
|
||||||
const { isOpen, close } = useViewParamsEditorStore();
|
const { isOpen, close } = useViewParamsEditorStore();
|
||||||
|
const clearSavedParams = useSavedViewParams((store) => store.clear);
|
||||||
const isSmallScreen = useIsSmallScreen();
|
const isSmallScreen = useIsSmallScreen();
|
||||||
|
|
||||||
const getPreservedParams = () => getPreservedSearchParams(searchParams, viewOptions);
|
const getPreservedParams = () => getPreservedSearchParams(searchParams, viewOptions);
|
||||||
@@ -37,6 +38,7 @@ function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetParams = () => {
|
const resetParams = () => {
|
||||||
|
clearSavedParams(target);
|
||||||
setSearchParams(getPreservedParams());
|
setSearchParams(getPreservedParams());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSearchParams } from 'react-router';
|
|
||||||
import { OntimeView, URLPreset } from 'ontime-types';
|
import { OntimeView, URLPreset } from 'ontime-types';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { useViewUrlPresets } from '../../hooks-query/useUrlPresets';
|
import { useViewUrlPresets } from '../../hooks-query/useUrlPresets';
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user