Compare commits

..

46 Commits

Author SHA1 Message Date
Carlos Valente 2a2554d32a refactor: prevent custom field overriding prototype property 2026-03-14 07:42:40 +01:00
Carlos Valente a7a78702b1 refactor: icon compatibility with macos tahoe 2026-03-13 08:10:48 +01:00
Carlos Valente 26d887c1a6 fix: prevent issues with midnight and day recognition on dst kick in 2026-03-11 07:20:42 +01:00
Alex Christoffer Rasmussen ceb490c60a fix: edit custom fields with type image (#1997) 2026-03-08 19:31:00 +01:00
Carlos Valente 1849b4d39f Deps migration (#1988)
* chore: migrate eslint to oxlint

* chore: migrate prettier to oxfmt

* chore: migrate typescript

* chore: toThrow should have a expected value

* chore: cast test value as Day

* chore: small title fix

* chore: mocks should be hoisted

* chore: incorrect async useage

* chore: test should be inside description

* chore: test sohuld include an expeced

* chore: oxfmt

---------

Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
2026-03-08 16:22:12 +01:00
alex-Arc 9516ac21bb fix: utf-8 from project file 2026-03-07 08:25:34 +01:00
alex-Arc 0e173137e8 add build-in translations 2026-03-07 08:25:23 +01:00
Joel Wetzell 1202ef3e80 add translation keys for timer message labels 2026-03-07 08:25:23 +01:00
Carlos Valente 2ef0b81743 refactor: extract midnight cross rules 2026-03-03 20:48:53 +01:00
Carlos Valente 5a5450d7ef refactor: use Day type in client 2026-03-03 20:48:53 +01:00
Carlos Valente 6f1f10ccff fix: backdate rundown start when rolling 2026-03-03 20:48:53 +01:00
Carlos Valente 5ff7861968 fix: use rundown day for calculations 2026-03-03 20:48:53 +01:00
Carlos Valente f1a1c358bc fix: infer day offset from epoch 2026-03-03 20:48:53 +01:00
Carlos Valente efd19353e9 fix: roll resets day 2026-03-03 20:48:53 +01:00
Carlos Valente a29a493678 fix: add current day when ticking over midnight 2026-03-03 20:48:53 +01:00
Carlos Valente 27b179276d fix: skip handles events across midnight 2026-03-03 20:48:53 +01:00
Carlos Valente 155cf48a17 fix: roll loads events that cross midnight 2026-03-03 20:48:53 +01:00
Alex Christoffer Rasmussen 358ad79ae4 Automation should use event store for data (#1962)
* fix: automation should use RuntimeStore for data

* add: aux timer template help
2026-03-02 20:54:46 +01:00
Alex Christoffer Rasmussen c1fcdf7065 Remove serverport from project file (#1957)
* feat: get server port from app satate or env

optional startup port from env will always override

function for parsing port from env

populate default port in app state

add test for migration

* bump version
2026-03-02 06:02:29 -08:00
Carlos Valente 1fe58e21be fix: onStart triggered by pending roll start 2026-02-28 10:32:19 +01:00
Carlos Valente 3c0e7ba4d5 chore: fix import across packages 2026-02-27 20:32:02 +01:00
Carlos Valente e8f159d894 feat: create branded types to determine different handling of time 2026-02-27 20:32:02 +01:00
Carlos Valente 168d7103b1 fix: prevent settings reflow on small width screens 2026-02-23 20:20:07 +01:00
Carlos Valente 63d7250a70 refactor: consistent focus styling 2026-02-23 20:19:49 +01:00
Carlos Valente 88f21e2186 refactor: improve timer styling in playback controls 2026-02-23 20:19:49 +01:00
Carlos Valente d9a18b0c42 fix: path comparison considers core params 2026-02-23 15:23:49 +01:00
Carlos Valente df588b8845 refactor: extract cuesheet permissions 2026-02-23 15:23:49 +01:00
Carlos Valente 5be504ce8f fix: prevent presets losing params 2026-02-23 15:23:49 +01:00
Carlos Valente 6268e327b9 refactor: mask cuesheet presets 2026-02-23 15:23:49 +01:00
Carlos Valente 1c81b3a23c refactor: hide edit button for users with no write permissions 2026-02-23 15:23:49 +01:00
Carlos Valente d28895ce9c refactor: prevent import map collision across stages 2026-02-23 15:22:56 +01:00
Carlos Valente 1a23287863 refactor: osc disabled by default to prevent collisions 2026-02-17 21:23:34 +01:00
Alex Christoffer Rasmussen 0b3ba65df3 feat: allow existing custom fields to keep its type on excel import (#1976) 2026-02-17 07:59:27 +08:00
Carlos Valente c3a7cd6a39 bump version to 4.4.2 2026-02-16 06:58:08 +01:00
Carlos Valente 39e6c15adf fix: hide now and next cards if there is no content 2026-02-16 06:58:08 +01:00
Carlos Valente aec6673b07 refactor: improve connection handling
- only invalidate caches on reconnect
- add jitter to avoid overwhelming server and allow cache to stabilise
- prevent multiple reconnect timers
2026-02-14 12:08:04 +01:00
Carlos Valente 9e8cb1c755 refactor: improve request cancellation and timeout handling 2026-02-14 12:08:04 +01:00
Carlos Valente cdf02f7c3f bump version to 4.4.1 2026-02-14 12:07:45 +01:00
Carlos Valente 77779a4648 refactor: debounce writing to disk 2026-02-14 09:30:24 +01:00
Carlos Valente e16ec3f388 refactor: prevent suspending page on first add 2026-02-14 08:56:16 +01:00
Carlos Valente 63cfc9ec19 refactor: optimistic add elements in UI 2026-02-14 08:56:16 +01:00
Carlos Valente 4827117163 refactor: extract entry creation logic 2026-02-14 08:56:16 +01:00
Carlos Valente 5777ef3532 refactor: extract parent resolution 2026-02-14 08:56:16 +01:00
Carlos Valente 80a3b04493 refactor: export rundown placement logic 2026-02-14 08:56:16 +01:00
Carlos Valente 7c5fdd3b19 refactor: remove unused exports 2026-02-14 08:56:16 +01:00
Carlos Valente 31fd8e5200 fix: prevent infinite render in settings forms 2026-02-13 21:12:29 +01:00
570 changed files with 6195 additions and 6007 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# Ignore config files # Ignore formatter config files
.prettier* .oxfmt*
# Ignore install stuff # Ignore install stuff
**/yarn.lock **/yarn.lock
-2
View File
@@ -1,2 +0,0 @@
"ONTIME_VERSION.js"
dist/
-53
View File
@@ -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
View File
@@ -1,2 +1,2 @@
github: cpvalente github: cpvalente
buy_me_a_coffee: cpvalente buy_me_a_coffee: cpvalente
+1 -1
View File
@@ -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 }}
+2 -1
View File
@@ -33,6 +33,7 @@ e2e/tests/fixtures/tmp/*
# production # production
build/ build/
dist/ dist/
*.tsbuildinfo
# bundled assets # bundled assets
translations.json translations.json
@@ -51,4 +52,4 @@ ontime-data/
**/ONTIME_VERSION.js **/ONTIME_VERSION.js
# temporary write files # temporary write files
**.tmp **.tmp
+25
View File
@@ -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"
]
}
+37
View File
@@ -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"
]
}
-14
View File
@@ -1,14 +0,0 @@
build
coverage
dist
node_modules
playwright-report
pnpm-lock.yaml
**/*.toml
**/*.json
!tsconfig.common.json
!turbo.json
!package.json
-9
View File
@@ -1,9 +0,0 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120
}
+24 -22
View File
@@ -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,24 @@ While it should allow for a generic setup, it might need to be modified to fit y
From the project root, run the following commands From the project root, run the following commands
- __Build docker image from__ by running `docker build -t getontime/ontime .` - **Build docker image from** by running `docker build -t getontime/ontime .`
- __Run docker image from compose__ by running `docker-compose up -d` - **Run docker image from compose** by running `docker-compose up -d`
Other useful commands Other useful commands
- __List running processes__ by running `docker ps` - **List running processes** by running `docker ps`
- __Kill running process__ by running `docker kill <process-id>` - **Kill running process** by running `docker kill <process-id>`
## CONTRIBUTION GUIDELINES ## 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
+7 -1
View File
@@ -7,6 +7,7 @@
[![](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=%E2%9D%A4&logo=buymeacoffee&color=%23fe8e86)](https://www.buymeacoffee.com/cpvalente) [![](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=%E2%9D%A4&logo=buymeacoffee&color=%23fe8e86)](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)
+5
View File
@@ -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.
-16
View File
@@ -1,16 +0,0 @@
{
"parserOptions": {
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
},
"extends": [
"eslint:recommended"
],
"plugins": [],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"extends": ["../../.oxlintrc.json"],
"plugins": ["unicorn", "typescript", "oxc", "vitest"]
}
-9
View File
@@ -1,9 +0,0 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120
}
+9 -1
View File
@@ -1,6 +1,7 @@
# Ontime CLI # Ontime CLI
![Ontime](https://github.com/cpvalente/ontime/blob/9e63261b350e5b7b61c7ffb17c9e75a6c5a8c711/.github/aux-images/editor.png) ![Ontime](https://github.com/cpvalente/ontime/blob/9e63261b350e5b7b61c7ffb17c9e75a6c5a8c711/.github/aux-images/editor.png)
> 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
View File
@@ -1,27 +1,22 @@
{ {
"name": "@getontime/cli", "name": "@getontime/cli",
"version": "4.4.0", "version": "4.5.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:"
}
}
-2
View File
@@ -1,2 +0,0 @@
node_modules
build
-142
View File
@@ -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)$"
]
]
}
]
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"extends": ["../../.oxlintrc.json"],
"plugins": ["unicorn", "typescript", "oxc", "react", "vitest"]
}
-9
View File
@@ -1,9 +0,0 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120
}
+3 -15
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "4.4.0", "version": "4.5.0",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -37,8 +37,8 @@
"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",
@@ -58,25 +58,13 @@
}, },
"devDependencies": { "devDependencies": {
"@sentry/vite-plugin": "^2.16.1", "@sentry/vite-plugin": "^2.16.1",
"@tanstack/eslint-plugin-query": "^5.8.4",
"@types/prismjs": "^1.26.5", "@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:",
"@typescript-eslint/parser": "catalog:",
"@vitejs/plugin-react": "4.5.1", "@vitejs/plugin-react": "4.5.1",
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"eslint": "catalog:",
"eslint-config-prettier": "catalog:",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-prettier": "catalog:",
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-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": "6.3.1",
+1 -3
View File
@@ -1,9 +1,7 @@
{ {
"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": "#2B5ABC", "theme_color": "#2B5ABC",
"background_color": "#101010", "background_color": "#101010",
"display": "standalone" "display": "standalone"
+5 -3
View File
@@ -1,17 +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
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();
+5 -5
View File
@@ -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'));
+6 -6
View File
@@ -2,16 +2,16 @@ 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';
const assetsPath = `${apiEntryUrl}/assets`; const assetsPath = `${apiEntryUrl}/assets`;
/** /**
* HTTP request to get css contents * HTTP request to get css contents
*/ */
export async function getCSSContents(): Promise<string> { export async function getCSSContents(options?: RequestOptions): Promise<string> {
const res = await axios.get(`${assetsPath}/css`); const res = await axios.get(`${assetsPath}/css`, { signal: options?.signal });
return res.data; return res.data;
} }
@@ -35,8 +35,8 @@ export async function restoreCSSContents(): Promise<string> {
/** /**
* HTTP request to get user translation * HTTP request to get user translation
*/ */
export async function getUserTranslation(): Promise<TranslationObject> { export async function getUserTranslation(options?: RequestOptions): Promise<TranslationObject> {
const res = await axios.get(customTranslationsURL); const res = await axios.get(customTranslationsURL, { signal: options?.signal });
return res.data; return res.data;
} }
+3 -2
View File
@@ -9,14 +9,15 @@ import type {
} from 'ontime-types'; } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const automationsPath = `${apiEntryUrl}/automations`; const automationsPath = `${apiEntryUrl}/automations`;
/** /**
* HTTP request to get the automations settings * HTTP request to get the automations settings
*/ */
export async function getAutomationSettings(): Promise<AutomationSettings> { export async function getAutomationSettings(options?: RequestOptions): Promise<AutomationSettings> {
const res = await axios.get(automationsPath); const res = await axios.get(automationsPath, { signal: options?.signal });
return res.data; return res.data;
} }
+4 -1
View File
@@ -1,5 +1,8 @@
import axios from 'axios'; import axios from 'axios';
import { axiosConfig } from './requestTimeouts';
axios.defaults.validateStatus = (status) => { axios.defaults.validateStatus = (status) => {
return (status >= 200 && status < 300) || status === 304; return status >= 200 && status < 300;
}; };
axios.defaults.timeout = axiosConfig.shortTimeout;
+3 -2
View File
@@ -2,14 +2,15 @@ import axios from 'axios';
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types'; import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const customFieldsPath = `${apiEntryUrl}/custom-fields`; const customFieldsPath = `${apiEntryUrl}/custom-fields`;
/** /**
* Requests list of known custom fields * Requests list of known custom fields
*/ */
export async function getCustomFields(): Promise<CustomFields> { export async function getCustomFields(options?: RequestOptions): Promise<CustomFields> {
const res = await axios.get(customFieldsPath); const res = await axios.get(customFieldsPath, { signal: options?.signal });
return res.data; return res.data;
} }
+9 -3
View File
@@ -2,6 +2,8 @@ import axios, { AxiosResponse } from 'axios';
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types'; import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
import { createBlob, downloadBlob } from './utils'; import { createBlob, downloadBlob } from './utils';
const dbPath = `${apiEntryUrl}/db`; const dbPath = `${apiEntryUrl}/db`;
@@ -10,7 +12,7 @@ const dbPath = `${apiEntryUrl}/db`;
* HTTP request to the current DB * HTTP request to the current DB
*/ */
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> { export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
return axios.post(`${dbPath}/download`, { filename }); return axios.post(`${dbPath}/download`, { filename }, { timeout: axiosConfig.longTimeout });
} }
/** /**
@@ -37,6 +39,7 @@ export async function uploadProjectFile(file: File): Promise<MessageResponse> {
const formData = new FormData(); const formData = new FormData();
formData.append('project', file); formData.append('project', file);
const response = await axios.post(`${dbPath}/upload`, formData, { const response = await axios.post(`${dbPath}/upload`, formData, {
timeout: axiosConfig.longTimeout,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
@@ -76,8 +79,11 @@ export async function quickProject(data: QuickStartData): Promise<MessageRespons
/** /**
* HTTP request to get the list of available project files * HTTP request to get the list of available project files
*/ */
export async function getProjects(): Promise<ProjectFileListResponse> { export async function getProjects(options?: RequestOptions): Promise<ProjectFileListResponse> {
const res = await axios.get(`${dbPath}/all`); const res = await axios.get(`${dbPath}/all`, {
signal: options?.signal,
timeout: options?.timeout,
});
return res.data; return res.data;
} }
+22 -6
View File
@@ -3,6 +3,8 @@ import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { ImportMap } from 'ontime-utils'; import { ImportMap } from 'ontime-utils';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
import { downloadBlob } from './utils'; import { downloadBlob } from './utils';
const excelPath = `${apiEntryUrl}/excel`; const excelPath = `${apiEntryUrl}/excel`;
@@ -11,10 +13,12 @@ const excelPath = `${apiEntryUrl}/excel`;
* upload Excel file to server * upload Excel file to server
* @return string - file ID op the uploaded file * @return string - file ID op the uploaded file
*/ */
export async function upload(file: File): Promise<string[]> { export async function upload(file: File, requestOptions?: RequestOptions): Promise<string[]> {
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 = await axios.post(`${excelPath}/upload`, formData, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
@@ -27,19 +31,31 @@ type PreviewSpreadsheetResponse = {
customFields: CustomFields; customFields: CustomFields;
summary: RundownSummary; summary: RundownSummary;
}; };
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> { export async function importRundownPreview(
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, { options: ImportMap,
options, requestOptions?: RequestOptions,
}); ): Promise<PreviewSpreadsheetResponse> {
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
`${excelPath}/preview`,
{
options,
},
{
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
},
);
return response.data; return response.data;
} }
/** /**
* Downloads a xlsx representation of the rundown from the server * Downloads a xlsx representation of the rundown from the server
*/ */
export async function downloadAsExcel(rundownId: string, fileName?: string) { export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) {
try { try {
const response = await axios.get(`${excelPath}/${rundownId}/export`, { const response = await axios.get(`${excelPath}/${rundownId}/export`, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
responseType: 'blob', responseType: 'blob',
}); });
+3 -2
View File
@@ -1,6 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { apiRepoLatest } from '../../externals'; import { apiRepoLatest } from '../../externals';
import type { RequestOptions } from './requestOptions';
export type HasUpdate = { export type HasUpdate = {
url: string; url: string;
@@ -10,8 +11,8 @@ export type HasUpdate = {
/** /**
* HTTP request to get the latest version and url from github * HTTP request to get the latest version and url from github
*/ */
export async function getLatestVersion(): Promise<HasUpdate> { export async function getLatestVersion(options?: RequestOptions): Promise<HasUpdate> {
const res = await axios.get(apiRepoLatest); const res = await axios.get(apiRepoLatest, { signal: options?.signal });
return { return {
url: res.data.html_url as string, url: res.data.html_url as string,
version: res.data.tag_name as string, version: res.data.tag_name as string,
+8 -2
View File
@@ -2,14 +2,19 @@ import axios, { AxiosResponse } from 'axios';
import { ProjectData, ProjectLogoResponse } from 'ontime-types'; import { ProjectData, ProjectLogoResponse } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
const projectPath = `${apiEntryUrl}/project`; const projectPath = `${apiEntryUrl}/project`;
/** /**
* HTTP request to fetch project data * HTTP request to fetch project data
*/ */
export async function getProjectData(): Promise<ProjectData> { export async function getProjectData(options?: RequestOptions): Promise<ProjectData> {
const res = await axios.get(projectPath); const res = await axios.get(projectPath, {
signal: options?.signal,
timeout: options?.timeout,
});
return res.data; return res.data;
} }
@@ -27,6 +32,7 @@ export async function uploadProjectLogo(file: File): Promise<AxiosResponse<Proje
const formData = new FormData(); const formData = new FormData();
formData.append('image', file); formData.append('image', file);
const response = await axios.post(`${projectPath}/upload`, formData, { const response = await axios.post(`${projectPath}/upload`, formData, {
timeout: axiosConfig.longTimeout,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
+4 -4
View File
@@ -2,16 +2,16 @@ import axios from 'axios';
import { OntimeReport } from 'ontime-types'; import { OntimeReport } from 'ontime-types';
import { ontimeQueryClient } from '../../common/queryClient'; import { ontimeQueryClient } from '../../common/queryClient';
import { REPORT, apiEntryUrl } from './constants';
import { apiEntryUrl, REPORT } from './constants'; import type { RequestOptions } from './requestOptions';
export const reportUrl = `${apiEntryUrl}/report`; export const reportUrl = `${apiEntryUrl}/report`;
/** /**
* HTTP request to fetch all reports * HTTP request to fetch all reports
*/ */
export async function fetchReport(): Promise<OntimeReport> { export async function fetchReport(options?: RequestOptions): Promise<OntimeReport> {
const res = await axios.get(reportUrl); const res = await axios.get(reportUrl, { signal: options?.signal });
return res.data; return res.data;
} }
@@ -0,0 +1,4 @@
export type RequestOptions = {
signal?: AbortSignal;
timeout?: number;
};
@@ -0,0 +1,8 @@
/**
* Keep a short global timeout for regular API requests, but allow
* longer windows for file transfer and heavy import/export operations.
*/
export const axiosConfig = {
shortTimeout: 20 * 1000, // 20 seconds
longTimeout: 3 * 60 * 1000, // 3 minutes
} as const;
+5 -4
View File
@@ -2,6 +2,7 @@ import axios, { AxiosResponse } from 'axios';
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types'; import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
type RundownId = string; type RundownId = string;
const rundownPath = `${apiEntryUrl}/rundowns`; const rundownPath = `${apiEntryUrl}/rundowns`;
@@ -11,16 +12,16 @@ const rundownPath = `${apiEntryUrl}/rundowns`;
/** /**
* HTTP request to fetch a list of existing rundowns * HTTP request to fetch a list of existing rundowns
*/ */
export async function fetchProjectRundownList(): Promise<ProjectRundownsList> { export async function fetchProjectRundownList(options?: RequestOptions): Promise<ProjectRundownsList> {
const res = await axios.get(rundownPath); const res = await axios.get(rundownPath, { signal: options?.signal });
return res.data; return res.data;
} }
/** /**
* HTTP request to fetch all entries in the currently loaded rundown * HTTP request to fetch all entries in the currently loaded rundown
*/ */
export async function fetchCurrentRundown(): Promise<Rundown> { export async function fetchCurrentRundown(options?: RequestOptions): Promise<Rundown> {
const res = await axios.get(`${rundownPath}/current`); const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal });
if (!isValidRundown(res.data)) { if (!isValidRundown(res.data)) {
throw new Error('Invalid rundown payload'); throw new Error('Invalid rundown payload');
} }
+3 -2
View File
@@ -2,14 +2,15 @@ import axios from 'axios';
import { GetInfo, LinkOptions } from 'ontime-types'; import { GetInfo, LinkOptions } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const sessionPath = `${apiEntryUrl}/session`; const sessionPath = `${apiEntryUrl}/session`;
/** /**
* HTTP request to retrieve application info * HTTP request to retrieve application info
*/ */
export async function getInfo(): Promise<GetInfo> { export async function getInfo(options?: RequestOptions): Promise<GetInfo> {
const res = await axios.get(`${sessionPath}/info`); const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal });
return res.data; return res.data;
} }
+19 -3
View File
@@ -1,15 +1,16 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { Settings } from 'ontime-types'; import { PortInfo, Settings } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const settingsPath = `${apiEntryUrl}/settings`; const settingsPath = `${apiEntryUrl}/settings`;
/** /**
* HTTP request to retrieve application settings * HTTP request to retrieve application settings
*/ */
export async function getSettings(): Promise<Settings> { export async function getSettings(options?: RequestOptions): Promise<Settings> {
const res = await axios.get(settingsPath); const res = await axios.get(settingsPath, { signal: options?.signal });
return res.data; return res.data;
} }
@@ -26,3 +27,18 @@ export async function postSettings(data: Settings): Promise<AxiosResponse<Settin
export async function postShowWelcomeDialog(show: boolean) { export async function postShowWelcomeDialog(show: boolean) {
axios.post(`${settingsPath}/welcomedialog`, { show }); axios.post(`${settingsPath}/welcomedialog`, { show });
} }
/**
* HTTP request to retrieve server port
*/
export async function getServerPort(): Promise<PortInfo> {
const res = await axios.get(`${settingsPath}/serverport`);
return res.data;
}
/**
* HTTP request to set server port
*/
export async function postServerPort(serverPort: number): Promise<AxiosResponse<PortInfo>> {
return axios.post(`${settingsPath}/serverport`, { serverPort });
}
+32 -5
View File
@@ -3,6 +3,8 @@ import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ont
import { ImportMap } from 'ontime-utils'; import { ImportMap } from 'ontime-utils';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
const sheetsPath = `${apiEntryUrl}/sheets`; const sheetsPath = `${apiEntryUrl}/sheets`;
@@ -23,6 +25,7 @@ export const verifyAuthenticationStatus = async (): Promise<{
export const requestConnection = async ( export const requestConnection = async (
file: File, file: File,
sheetId: string, sheetId: string,
requestOptions?: RequestOptions,
): Promise<{ ): Promise<{
verification_url: string; verification_url: string;
user_code: string; user_code: string;
@@ -31,6 +34,8 @@ export const requestConnection = async (
formData.append('client_secret', file); formData.append('client_secret', file);
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, { const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
@@ -53,24 +58,46 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
export const previewRundown = async ( export const previewRundown = async (
sheetId: string, sheetId: string,
options: ImportMap, options: ImportMap,
requestOptions?: RequestOptions,
): Promise<{ ): Promise<{
rundown: Rundown; rundown: Rundown;
customFields: CustomFields; customFields: CustomFields;
summary: RundownSummary; summary: RundownSummary;
}> => { }> => {
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options }); const response = await axios.post(
`${sheetsPath}/${sheetId}/read`,
{ options },
{
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
},
);
return response.data; return response.data;
}; };
export const getWorksheetNames = async (sheetId: string): Promise<string[]> => { export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise<string[]> => {
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`); const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`, undefined, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
});
return response.data; return response.data;
}; };
/** /**
* HTTP request to upload the rundown to a google sheet * HTTP request to upload the rundown to a google sheet
*/ */
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => { export const uploadRundown = async (
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options }); sheetId: string,
options: ImportMap,
requestOptions?: RequestOptions,
): Promise<void> => {
const response = await axios.post(
`${sheetsPath}/${sheetId}/write`,
{ options },
{
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
},
);
return response.data; return response.data;
}; };
+3 -2
View File
@@ -2,14 +2,15 @@ import axios from 'axios';
import { URLPreset } from 'ontime-types'; import { URLPreset } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const urlPresetsPath = `${apiEntryUrl}/url-presets`; const urlPresetsPath = `${apiEntryUrl}/url-presets`;
/** /**
* HTTP request to retrieve all presets * HTTP request to retrieve all presets
*/ */
export async function getUrlPresets(): Promise<URLPreset[]> { export async function getUrlPresets(options?: RequestOptions): Promise<URLPreset[]> {
const res = await axios.get(urlPresetsPath); const res = await axios.get(urlPresetsPath, { signal: options?.signal });
return res.data; return res.data;
} }
+3 -2
View File
@@ -2,6 +2,7 @@ import axios from 'axios';
import type { ViewSettings } from 'ontime-types'; import type { ViewSettings } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const viewSettingsPath = `${apiEntryUrl}/view-settings`; const viewSettingsPath = `${apiEntryUrl}/view-settings`;
@@ -9,8 +10,8 @@ const viewSettingsPath = `${apiEntryUrl}/view-settings`;
* HTTP request to fetch view settings * HTTP request to fetch view settings
* @returns * @returns
*/ */
export async function getViewSettings(): Promise<ViewSettings> { export async function getViewSettings(options?: RequestOptions): Promise<ViewSettings> {
const res = await axios.get(viewSettingsPath); const res = await axios.get(viewSettingsPath, { signal: options?.signal });
return res.data; return res.data;
} }
@@ -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,7 +1,7 @@
/* 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';
/* eslint-disable react/destructuring-assignment */
import React from 'react';
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket'; import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
import { runtimeStore } from '../../stores/runtime'; import { runtimeStore } from '../../stores/runtime';
@@ -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';
@@ -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,5 +1,5 @@
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;
@@ -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,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';
@@ -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);
@@ -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,9 +1,9 @@
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';
@@ -13,7 +13,6 @@ 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';
@@ -1,11 +1,10 @@
import { memo } from 'react';
import { useDisclosure, useHotkeys } from '@mantine/hooks'; import { useDisclosure, useHotkeys } from '@mantine/hooks';
import { memo } from 'react';
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 */
@@ -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';
@@ -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 {
@@ -15,6 +15,11 @@ $thumb-color-hover: $white-60;
height: 100%; height: 100%;
width: 100%; width: 100%;
overscroll-behavior: contain; overscroll-behavior: contain;
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: -2px;
}
} }
.scrollbar { .scrollbar {
@@ -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';
@@ -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';
@@ -10,7 +10,7 @@
.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: 600px;
@@ -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,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,14 @@
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 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';
@@ -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';
@@ -47,4 +47,4 @@
.hidden { .hidden {
display: none; display: none;
} }
@@ -1,8 +1,7 @@
import { IoChevronDown } from 'react-icons/io5';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import { IoChevronDown } from 'react-icons/io5';
import { cx } from '../../utils/styleUtils'; import { cx } from '../../utils/styleUtils';
import { OptionTitle } from './constants'; import { OptionTitle } from './constants';
import ParamInput from './ParamInput'; import ParamInput from './ParamInput';
import { type ParamField } from './viewParams.types'; import { type ParamField } from './viewParams.types';
@@ -1,7 +1,6 @@
import type { CustomFields, ProjectData } from 'ontime-types'; import type { CustomFields, ProjectData } from 'ontime-types';
import type { SelectOption } from '../select/Select'; import type { SelectOption } from '../select/Select';
import type { MultiselectOption, ViewOption } from './viewParams.types'; import type { MultiselectOption, ViewOption } from './viewParams.types';
/** /**
@@ -1,4 +1,4 @@
import { createContext, PropsWithChildren, useCallback, useEffect, useState } from 'react'; import { PropsWithChildren, createContext, useCallback, useEffect, useState } from 'react';
import useSettings from '../hooks-query/useSettings'; import useSettings from '../hooks-query/useSettings';
@@ -1,4 +1,4 @@
import { createContext, PropsWithChildren, useContext } from 'react'; import { PropsWithChildren, createContext, useContext } from 'react';
import { useEntryActions } from '../hooks/useEntryAction'; import { useEntryActions } from '../hooks/useEntryAction';
@@ -1,4 +1,4 @@
import { createContext } from 'react';
import { URLPreset } from 'ontime-types'; import { URLPreset } from 'ontime-types';
import { createContext } from 'react';
export const PresetContext = createContext<URLPreset | undefined>(undefined); export const PresetContext = createContext<URLPreset | undefined>(undefined);
@@ -4,7 +4,7 @@ import { dayInMs } from 'ontime-utils';
import { version } from '../../../../../package.json'; import { version } from '../../../../../package.json';
import { isLocalhost } from '../../externals'; import { isLocalhost } from '../../externals';
import { APP_VERSION } from '../api/constants'; import { APP_VERSION } from '../api/constants';
import { getLatestVersion, HasUpdate } from '../api/external'; import { HasUpdate, getLatestVersion } from '../api/external';
const placeholder: HasUpdate & { hasUpdates: boolean } = { url: '', version: '', hasUpdates: false }; const placeholder: HasUpdate & { hasUpdates: boolean } = { url: '', version: '', hasUpdates: false };
@@ -17,7 +17,7 @@ export default function useAppVersion() {
refetch, refetch,
} = useQuery({ } = useQuery({
queryKey: APP_VERSION, queryKey: APP_VERSION,
queryFn: getLatestVersion, queryFn: ({ signal }) => getLatestVersion({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,
@@ -8,12 +8,9 @@ import { automationPlaceholderSettings } from '../models/AutomationSettings';
export default function useAutomationSettings() { export default function useAutomationSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({ const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: AUTOMATION, queryKey: AUTOMATION,
queryFn: getAutomationSettings, queryFn: ({ signal }) => getAutomationSettings({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt: number) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
}); });
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch }; return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
@@ -10,12 +10,9 @@ const placeholder: CustomFields = {};
export default function useCustomFields() { export default function useCustomFields() {
const { data, status, isFetching, isError, refetch } = useQuery({ const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: CUSTOM_FIELDS, queryKey: CUSTOM_FIELDS,
queryFn: getCustomFields, queryFn: ({ signal }) => getCustomFields({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
}); });
return { data: data ?? placeholder, status, isFetching, isError, refetch }; return { data: data ?? placeholder, status, isFetching, isError, refetch };
@@ -8,7 +8,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
export function useCustomTranslation() { export function useCustomTranslation() {
const { data, status, refetch } = useQuery({ const { data, status, refetch } = useQuery({
queryKey: TRANSLATION, queryKey: TRANSLATION,
queryFn: getUserTranslation, queryFn: ({ signal }) => getUserTranslation({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
@@ -9,12 +9,9 @@ import { ontimePlaceholderInfo } from '../models/Info';
export default function useInfo() { export default function useInfo() {
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({ const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
queryKey: APP_INFO, queryKey: APP_INFO,
queryFn: getInfo, queryFn: ({ signal }) => getInfo({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
}); });
return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching }; return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching };
@@ -8,7 +8,7 @@ import { projectDataPlaceholder } from '../models/ProjectData';
export default function useProjectData() { export default function useProjectData() {
const { data, status, isFetching, isError, refetch } = useQuery({ const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: PROJECT_DATA, queryKey: PROJECT_DATA,
queryFn: getProjectData, queryFn: ({ signal }) => getProjectData({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { ProjectFile, ProjectFileList, ProjectFileListResponse } from 'ontime-types'; import { ProjectFile, ProjectFileList, ProjectFileListResponse } from 'ontime-types';
import { useMemo } from 'react';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { PROJECT_LIST } from '../api/constants'; import { PROJECT_LIST } from '../api/constants';
@@ -14,12 +14,9 @@ const placeholderProjectList: ProjectFileListResponse = {
function useProjectList() { function useProjectList() {
const { data, status, refetch } = useQuery({ const { data, status, refetch } = useQuery({
queryKey: PROJECT_LIST, queryKey: PROJECT_LIST,
queryFn: getProjects, queryFn: ({ signal }) => getProjects({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt: number) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
}); });
return { data: data ?? placeholderProjectList, status, refetch }; return { data: data ?? placeholderProjectList, status, refetch };
} }
@@ -3,7 +3,14 @@ import { ProjectRundownsList } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { PROJECT_RUNDOWNS } from '../api/constants'; import { PROJECT_RUNDOWNS } from '../api/constants';
import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList, loadRundown, renameRundown } from '../api/rundown'; import {
createRundown,
deleteRundown,
duplicateRundown,
fetchProjectRundownList,
loadRundown,
renameRundown,
} from '../api/rundown';
/** /**
* Project rundowns * Project rundowns
@@ -11,7 +18,7 @@ import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList
export function useProjectRundowns() { export function useProjectRundowns() {
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({ const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
queryKey: PROJECT_RUNDOWNS, queryKey: PROJECT_RUNDOWNS,
queryFn: fetchProjectRundownList, queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
@@ -30,7 +37,7 @@ export function useMutateProjectRundowns() {
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data); ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
}, },
}); });
const { mutateAsync: duplicate } = useMutation({ const { mutateAsync: duplicate } = useMutation({
mutationFn: duplicateRundown, mutationFn: duplicateRundown,
onMutate: () => { onMutate: () => {
@@ -40,7 +47,7 @@ export function useMutateProjectRundowns() {
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data); ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
}, },
}); });
const { mutateAsync: rename } = useMutation({ const { mutateAsync: rename } = useMutation({
mutationFn: ([rundownId, title]: Parameters<typeof renameRundown>) => renameRundown(rundownId, title), mutationFn: ([rundownId, title]: Parameters<typeof renameRundown>) => renameRundown(rundownId, title),
onMutate: () => { onMutate: () => {
@@ -8,11 +8,8 @@ import { fetchReport } from '../api/report';
export default function useReport() { export default function useReport() {
const { data, refetch } = useQuery<OntimeReport>({ const { data, refetch } = useQuery<OntimeReport>({
queryKey: REPORT, queryKey: REPORT,
queryFn: fetchReport, queryFn: ({ signal }) => fetchReport({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
networkMode: 'always',
staleTime: MILLIS_PER_HOUR, staleTime: MILLIS_PER_HOUR,
}); });
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { EntryId, OntimeEntry, Rundown } from 'ontime-types'; import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
import { useMemo } from 'react';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { RUNDOWN } from '../api/constants'; import { RUNDOWN } from '../api/constants';
@@ -24,7 +24,7 @@ const cachedRundownPlaceholder: Rundown = {
export default function useRundown() { export default function useRundown() {
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({ const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
queryKey: RUNDOWN, queryKey: RUNDOWN,
queryFn: fetchCurrentRundown, queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
@@ -8,7 +8,7 @@ import { ontimePlaceholderSettings } from '../models/OntimeSettings';
export default function useSettings() { export default function useSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({ const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: APP_SETTINGS, queryKey: APP_SETTINGS,
queryFn: getSettings, queryFn: ({ signal }) => getSettings({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
select: (data) => { select: (data) => {
const unobfuscated = { ...data }; const unobfuscated = { ...data };
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { OntimeView, URLPreset } from 'ontime-types'; import { OntimeView, URLPreset } from 'ontime-types';
import { useMemo } from 'react';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { URL_PRESETS } from '../api/constants'; import { URL_PRESETS } from '../api/constants';
@@ -13,7 +13,7 @@ interface FetchProps {
export default function useUrlPresets({ skip = false }: FetchProps = {}) { export default function useUrlPresets({ skip = false }: FetchProps = {}) {
const { data, status, isError, refetch } = useQuery({ const { data, status, isError, refetch } = useQuery({
queryKey: URL_PRESETS, queryKey: URL_PRESETS,
queryFn: getUrlPresets, queryFn: ({ signal }) => getUrlPresets({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
enabled: !skip, enabled: !skip,
@@ -9,7 +9,7 @@ import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
export default function useViewSettings() { export default function useViewSettings() {
const { data, status } = useQuery({ const { data, status } = useQuery({
queryKey: VIEW_SETTINGS, queryKey: VIEW_SETTINGS,
queryFn: getViewSettings, queryFn: ({ signal }) => getViewSettings({ signal }),
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
staleTime: MILLIS_PER_HOUR, staleTime: MILLIS_PER_HOUR,
}); });
@@ -1,11 +1,10 @@
import { MessageTag } from 'ontime-types';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router'; import { useLocation, useNavigate } from 'react-router';
import { MessageTag } from 'ontime-types';
import { useShallow } from 'zustand/shallow'; import { useShallow } from 'zustand/shallow';
import { useClientStore } from '../stores/clientStore'; import { useClientStore } from '../stores/clientStore';
import { sendSocket } from '../utils/socket'; import { sendSocket } from '../utils/socket';
import { useIsOnline } from './useSocket'; import { useIsOnline } from './useSocket';
export const useClientPath = () => { export const useClientPath = () => {
+115 -18
View File
@@ -1,31 +1,47 @@
import { useCallback, useMemo } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { import {
EntryId, EntryId,
InsertOptions, InsertOptions,
isOntimeEvent,
isOntimeGroup,
MaybeString, MaybeString,
OntimeDelay,
OntimeEntry, OntimeEntry,
OntimeEvent, OntimeEvent,
OntimeGroup,
OntimeMilestone,
PatchWithId,
Rundown, Rundown,
SupportedEntry, SupportedEntry,
TimeField, TimeField,
TimeStrategy, TimeStrategy,
TransientEventPayload, isOntimeEvent,
isOntimeGroup,
} from 'ontime-types'; } from 'ontime-types';
import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, swapEventData } from 'ontime-utils'; import {
MILLIS_PER_SECOND,
addToRundown,
createDelay,
createEvent,
createGroup,
createMilestone,
dayInMs,
generateId,
getInsertAfterId,
parseUserTime,
resolveInsertParent,
swapEventData,
} from 'ontime-utils';
import { useCallback, useMemo } from 'react';
import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils'; import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils';
import { RUNDOWN } from '../api/constants'; import { RUNDOWN } from '../api/constants';
import { import {
ReorderEntry,
deleteEntries, deleteEntries,
patchReorderEntry, patchReorderEntry,
postAddEntry, postAddEntry,
postCloneEntry, postCloneEntry,
putBatchEditEvents, putBatchEditEvents,
putEditEntry, putEditEntry,
ReorderEntry,
requestApplyDelay, requestApplyDelay,
requestDeleteAll, requestDeleteAll,
requestEventSwap, requestEventSwap,
@@ -85,9 +101,64 @@ export const useEntryActions = () => {
* @private * @private
*/ */
const { mutateAsync: addEntryMutation } = useMutation({ const { mutateAsync: addEntryMutation } = useMutation({
mutationFn: ([rundownId, entry]: Parameters<typeof postAddEntry>) => postAddEntry(rundownId, entry), mutationFn: ([rundownId, entry]: [string, PatchWithId & InsertOptions]) => postAddEntry(rundownId, entry),
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }), onMutate: async ([_rundownId, entry]) => {
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }), await queryClient.cancelQueries({ queryKey: RUNDOWN });
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
if (previousData) {
const optimisticEntry = createOptimisticEntry(entry);
const parentId = resolveInsertParent(previousData, entry);
const maybeParent = parentId ? previousData.entries[parentId] : null;
const parent = maybeParent && isOntimeGroup(maybeParent) ? maybeParent : null;
const afterId = getInsertAfterId(previousData, parent, entry.after, entry.before);
// create a mutable copy — addToRundown mutates in place using immutable array operations
const newRundown: Rundown = {
...previousData,
entries: { ...previousData.entries },
revision: -1,
};
// copy the parent group so we don't mutate the original
if (parent && parentId) {
newRundown.entries[parentId] = { ...parent, entries: [...parent.entries] };
}
addToRundown(
newRundown,
optimisticEntry,
afterId,
parent ? (newRundown.entries[parent.id] as OntimeGroup) : null,
);
queryClient.setQueryData<Rundown>(RUNDOWN, newRundown);
}
return { previousData };
},
onSuccess: (response) => {
if (!response.data) return;
const serverEntry = response.data;
const currentData = queryClient.getQueryData<Rundown>(RUNDOWN);
if (currentData) {
queryClient.setQueryData<Rundown>(RUNDOWN, {
...currentData,
entries: { ...currentData.entries, [serverEntry.id]: serverEntry },
});
}
},
onError: (_error, _variables, context) => {
if (context?.previousData) {
queryClient.setQueryData<Rundown>(RUNDOWN, context.previousData);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
}); });
/** /**
@@ -102,7 +173,15 @@ export const useEntryActions = () => {
throw new Error('Rundown not initialised'); throw new Error('Rundown not initialised');
} }
const newEntry: TransientEventPayload = { ...entry, id: generateId() }; const newEntry: PatchWithId & InsertOptions = { ...entry, id: generateId() };
// handle adding options that concern all event types
if (options?.after) {
newEntry.after = options.after;
}
if (options?.before) {
newEntry.before = options.before;
}
// ************* CHECK OPTIONS specific to events // ************* CHECK OPTIONS specific to events
if (isOntimeEvent(newEntry)) { if (isOntimeEvent(newEntry)) {
@@ -142,14 +221,6 @@ export const useEntryActions = () => {
} }
} }
// handle adding options that concern all event type
if (options?.after) {
(newEntry as TransientEventPayload).after = options.after;
}
if (options?.before) {
(newEntry as TransientEventPayload).before = options.before;
}
try { try {
await addEntryMutation([rundownId, newEntry]); await addEntryMutation([rundownId, newEntry]);
} catch (error) { } catch (error) {
@@ -905,3 +976,29 @@ function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) {
return { entries, order, flatOrder }; return { entries, order, flatOrder };
} }
/**
* Utility to create an optimistic entry for immediate cache insertion
*/
function createOptimisticEntry(payload: PatchWithId & InsertOptions): OntimeEntry {
const { after: _after, before: _before, ...entryData } = payload;
const id = entryData.id;
let parent: EntryId | null = null;
if ('parent' in entryData && entryData.parent) {
parent = entryData.parent;
}
switch (entryData.type) {
case SupportedEntry.Event: {
return createEvent({ ...entryData, id, parent }) as OntimeEvent;
}
case SupportedEntry.Delay:
return createDelay({ id, duration: (entryData as Partial<OntimeDelay>).duration ?? 0, parent });
case SupportedEntry.Group:
return createGroup({ ...(entryData as Partial<OntimeGroup>), id });
case SupportedEntry.Milestone:
return createMilestone({ ...(entryData as Partial<OntimeMilestone>), id, parent });
default:
throw new Error('Unknown entry type');
}
}
@@ -1,5 +1,5 @@
import { RefObject, useCallback, useEffect } from 'react';
import { MaybeString } from 'ontime-types'; import { MaybeString } from 'ontime-types';
import { RefObject, useCallback, useEffect } from 'react';
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>( function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: RefObject<ComponentRef>, componentRef: RefObject<ComponentRef>,
@@ -1,5 +1,5 @@
import { RefObject, useCallback, useEffect } from 'react';
import { EntryId } from 'ontime-types'; import { EntryId } from 'ontime-types';
import { RefObject, useCallback, useEffect } from 'react';
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>( function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: RefObject<ComponentRef> | null, componentRef: RefObject<ComponentRef> | null,
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useOs, useViewportSize } from '@mantine/hooks'; import { useOs, useViewportSize } from '@mantine/hooks';
import { useMemo } from 'react';
export function useIsMobileDevice(): boolean { export function useIsMobileDevice(): boolean {
const { width } = useViewportSize(); const { width } = useViewportSize();

Some files were not shown because too many files have changed in this diff Show More