mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e40a0eebec | |||
| 6a40c66a7b | |||
| 2806177c72 | |||
| 4408bdb0d3 | |||
| 871a6b46c8 | |||
| fd39cab845 | |||
| 1cee679397 | |||
| a7a78702b1 | |||
| 26d887c1a6 | |||
| ceb490c60a | |||
| 1849b4d39f | |||
| 9516ac21bb | |||
| 0e173137e8 | |||
| 1202ef3e80 | |||
| 2ef0b81743 | |||
| 5a5450d7ef | |||
| 6f1f10ccff | |||
| 5ff7861968 | |||
| f1a1c358bc | |||
| efd19353e9 | |||
| a29a493678 | |||
| 27b179276d | |||
| 155cf48a17 | |||
| 358ad79ae4 | |||
| c1fcdf7065 | |||
| 1fe58e21be | |||
| 3c0e7ba4d5 | |||
| e8f159d894 | |||
| 168d7103b1 | |||
| 63d7250a70 | |||
| 88f21e2186 | |||
| d9a18b0c42 | |||
| df588b8845 | |||
| 5be504ce8f | |||
| 6268e327b9 | |||
| 1c81b3a23c | |||
| d28895ce9c | |||
| 1a23287863 | |||
| 0b3ba65df3 | |||
| c3a7cd6a39 | |||
| 39e6c15adf | |||
| aec6673b07 | |||
| 9e8cb1c755 | |||
| cdf02f7c3f | |||
| 77779a4648 | |||
| e16ec3f388 | |||
| 63cfc9ec19 | |||
| 4827117163 | |||
| 5777ef3532 | |||
| 80a3b04493 | |||
| 7c5fdd3b19 | |||
| 31fd8e5200 |
+2
-2
@@ -1,5 +1,5 @@
|
||||
# Ignore config files
|
||||
.prettier*
|
||||
# Ignore formatter config files
|
||||
.oxfmt*
|
||||
|
||||
# Ignore install stuff
|
||||
**/yarn.lock
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
"ONTIME_VERSION.js"
|
||||
dist/
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"],
|
||||
"extends": ["plugin:playwright/playwright-test"]
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"no-useless-concat": "warn",
|
||||
"prefer-template": "warn",
|
||||
"no-throw-literal": "error",
|
||||
"no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["warn", "error"]
|
||||
}
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
github: cpvalente
|
||||
buy_me_a_coffee: cpvalente
|
||||
buy_me_a_coffee: cpvalente
|
||||
|
||||
@@ -64,4 +64,4 @@ jobs:
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
+2
-1
@@ -33,6 +33,7 @@ e2e/tests/fixtures/tmp/*
|
||||
# production
|
||||
build/
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
|
||||
# bundled assets
|
||||
translations.json
|
||||
@@ -51,4 +52,4 @@ ontime-data/
|
||||
**/ONTIME_VERSION.js
|
||||
|
||||
# temporary write files
|
||||
**.tmp
|
||||
**.tmp
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"sortPackageJson": false,
|
||||
"sortImports": {},
|
||||
"ignorePatterns": [
|
||||
"build",
|
||||
"coverage",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"playwright-report",
|
||||
"pnpm-lock.yaml",
|
||||
"**/*.toml",
|
||||
"**/*.json",
|
||||
"!tsconfig.common.json",
|
||||
"!turbo.json",
|
||||
"!package.json"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"categories": {
|
||||
"correctness": "warn",
|
||||
"suspicious": "off",
|
||||
"pedantic": "off",
|
||||
"perf": "warn",
|
||||
"style": "off",
|
||||
"restriction": "off",
|
||||
"nursery": "off"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"caughtErrors": "none"
|
||||
}
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"apps/client/build/**",
|
||||
"apps/electron/assets/**",
|
||||
"**/ONTIME_VERSION.js"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
build
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
|
||||
playwright-report
|
||||
|
||||
|
||||
pnpm-lock.yaml
|
||||
**/*.toml
|
||||
**/*.json
|
||||
!tsconfig.common.json
|
||||
!turbo.json
|
||||
!package.json
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
+24
-22
@@ -2,16 +2,16 @@
|
||||
|
||||
Ontime consists of 3 distinct parts
|
||||
|
||||
- __client__: A React app for Ontime's UI and web clients
|
||||
- __electron__: An electron app which facilitates the cross-platform distribution of Ontime
|
||||
- __server__: A node application which handles the domains services and integrations
|
||||
- **client**: A React app for Ontime's UI and web clients
|
||||
- **electron**: An electron app which facilitates the cross-platform distribution of Ontime
|
||||
- **server**: A node application which handles the domains services and integrations
|
||||
|
||||
The steps below will assume you have locally installed the necessary dependencies.
|
||||
Other dependencies will be installed as part of the setup
|
||||
|
||||
- __node__ (~22)
|
||||
- __pnpm__ (~10)
|
||||
- __docker__ (only necessary to run and build docker images)
|
||||
- **node** (~22)
|
||||
- **pnpm** (~10)
|
||||
- **docker** (only necessary to run and build docker images)
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
- __Install the project dependencies__ by running `pnpm i`
|
||||
- __Build the UI and server__ by running `pnpm build`
|
||||
- __Create the package__ by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
||||
- **Install the project dependencies** by running `pnpm i`
|
||||
- **Build the UI and server** by running `pnpm build`
|
||||
- **Create the package** by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
||||
|
||||
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
|
||||
|
||||
- __Build docker image from__ by running `docker build -t getontime/ontime .`
|
||||
- __Run docker image from compose__ by running `docker-compose up -d`
|
||||
- **Build docker image from** by running `docker build -t getontime/ontime .`
|
||||
- **Run docker image from compose** by running `docker-compose up -d`
|
||||
|
||||
Other useful commands
|
||||
|
||||
- __List running processes__ by running `docker ps`
|
||||
- __Kill running process__ by running `docker kill <process-id>`
|
||||
- **List running processes** by running `docker ps`
|
||||
- **Kill running process** by running `docker kill <process-id>`
|
||||
|
||||
## CONTRIBUTION GUIDELINES
|
||||
|
||||
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:
|
||||
* [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
|
||||
* [ ] You have ran code formatting and linting in all your changes
|
||||
* [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
|
||||
* [ ] The PR body contains description and motivation for the changes
|
||||
|
||||
- [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
|
||||
- [ ] You have ran code formatting and linting in all your changes
|
||||
- [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
|
||||
- [ ] The PR body contains description and motivation for the changes
|
||||
|
||||
After this checklist is complete, you can request a review from one of the maintainers to get feedback and approval on the changes. \
|
||||
We will review as soon as possible
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[](https://www.buymeacoffee.com/cpvalente)
|
||||
|
||||
## 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>
|
||||
|
||||
- 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>
|
||||
|
||||
... or
|
||||
|
||||
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
||||
- Install from <a href="https://www.npmjs.com/package/@getontime/cli">NPM</a>
|
||||
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
||||
|
||||
## 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.
|
||||
|
||||
## Using Ontime?
|
||||
|
||||
Let us know!
|
||||
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
|
||||
|
||||
## Main features
|
||||
|
||||
- **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...
|
||||
- **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)
|
||||
|
||||
## Links
|
||||
- [Ontime website](https://getontime.no)
|
||||
|
||||
- [Ontime website](https://getontime.no)
|
||||
- [Documentation](https://docs.getontime.no)
|
||||
- [Video tutorials](https://www.youtube.com/@ontimeapp)
|
||||
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
# 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.
|
||||
|
||||
## Why sponsor
|
||||
|
||||
Ontime is and will remain free, regardless of your decision to contribute.
|
||||
Regardless, you may choose to provide a monetary contribution:
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"plugins": [],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.json"],
|
||||
"plugins": ["unicorn", "typescript", "oxc", "vitest"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
# Ontime CLI
|
||||
|
||||

|
||||
|
||||
> Ontime 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.
|
||||
@@ -10,33 +11,40 @@ The CLI tool is a fast and lightweight way of installing Ontime and is perfect f
|
||||
## Getting started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
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.
|
||||
|
||||
### Running Ontime
|
||||
|
||||
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
|
||||
npm install -g @getontime/cli
|
||||
```
|
||||
|
||||
... and run using the installed script
|
||||
|
||||
```bash
|
||||
ontime
|
||||
```
|
||||
|
||||
Or install and run ontime (the installation here is temporary for the duration of the session)
|
||||
|
||||
```bash
|
||||
npx install @getontime/cli
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Ontime's repository](https://github.com/cpvalente/ontime)
|
||||
- [Ontime's documentation](https://docs.getontime.no/)
|
||||
- [Ontime's website](https://getontime.no/)
|
||||
|
||||
## Sponsoring
|
||||
|
||||
You can help the development of this project or say thank you with a one time donation. \
|
||||
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
|
||||
|
||||
|
||||
+21
-26
@@ -1,27 +1,22 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "4.4.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
"keywords": [
|
||||
"lighdev",
|
||||
"ontime",
|
||||
"timer"
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "main.js",
|
||||
"bin": {
|
||||
"ontime": "main.js"
|
||||
},
|
||||
"files": [
|
||||
"client",
|
||||
"external",
|
||||
"server"
|
||||
],
|
||||
"devDependencies": {
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"prettier": "catalog:"
|
||||
}
|
||||
}
|
||||
"name": "@getontime/cli",
|
||||
"version": "4.5.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
"keywords": [
|
||||
"lighdev",
|
||||
"ontime",
|
||||
"timer"
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "main.js",
|
||||
"bin": {
|
||||
"ontime": "main.js"
|
||||
},
|
||||
"files": [
|
||||
"client",
|
||||
"external",
|
||||
"server"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
@@ -1,142 +0,0 @@
|
||||
{
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@tanstack/eslint-plugin-query/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"react-compiler",
|
||||
"simple-import-sort",
|
||||
"@tanstack/query"
|
||||
],
|
||||
"rules": {
|
||||
"react-compiler/react-compiler": "warn",
|
||||
"react/jsx-no-bind": [
|
||||
"error",
|
||||
{
|
||||
"ignoreRefs": true,
|
||||
"allowArrowFunctions": true,
|
||||
"allowFunctions": false,
|
||||
"allowBind": false,
|
||||
"ignoreDOMComponents": true
|
||||
}
|
||||
],
|
||||
"react/jsx-boolean-value": [
|
||||
"error",
|
||||
"never",
|
||||
{
|
||||
"always": []
|
||||
}
|
||||
],
|
||||
"react/jsx-handler-names": [
|
||||
"off",
|
||||
{
|
||||
"eventHandlerPrefix": "handle",
|
||||
"eventHandlerPropPrefix": "on"
|
||||
}
|
||||
],
|
||||
"react/no-danger": "warn",
|
||||
"react/no-deprecated": [
|
||||
"error"
|
||||
],
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-no-undef": "error",
|
||||
"react/jsx-closing-tag-location": "warn",
|
||||
"react/jsx-no-useless-fragment": "warn",
|
||||
"react/no-unescaped-entities": "error",
|
||||
"react/jsx-tag-spacing": [
|
||||
"error",
|
||||
{
|
||||
"closingSlash": "never",
|
||||
"beforeSelfClosing": "always",
|
||||
"afterOpening": "never",
|
||||
"beforeClosing": "never"
|
||||
}
|
||||
],
|
||||
"react/jsx-space-before-closing": [
|
||||
"off",
|
||||
"always"
|
||||
],
|
||||
"react/jsx-curly-brace-presence": [
|
||||
"error",
|
||||
{
|
||||
"props": "never",
|
||||
"children": "never"
|
||||
}
|
||||
],
|
||||
"react/destructuring-assignment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"react/no-unsafe": "off",
|
||||
"react/prop-types": [
|
||||
"error",
|
||||
{
|
||||
"ignore": [],
|
||||
"customValidators": [],
|
||||
"skipUndeclared": true
|
||||
}
|
||||
],
|
||||
"simple-import-sort/imports": "warn",
|
||||
"simple-import-sort/exports": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
// override "simple-import-sort" config
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx",
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"simple-import-sort/imports": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
// Packages `react` related packages come first.
|
||||
[
|
||||
"^react",
|
||||
"^@?\\w"
|
||||
],
|
||||
// Internal packages.
|
||||
[
|
||||
"^(@|components)(/.*|$)"
|
||||
],
|
||||
// Side effect imports.
|
||||
[
|
||||
"^\\u0000"
|
||||
],
|
||||
// Parent imports. Put `..` last.
|
||||
[
|
||||
"^\\.\\.(?!/?$)",
|
||||
"^\\.\\./?$"
|
||||
],
|
||||
// Other relative imports. Put same-folder imports and `.` last.
|
||||
[
|
||||
"^\\./(?=.*/)(?!/?$)",
|
||||
"^\\.(?!/?$)",
|
||||
"^\\./?$"
|
||||
],
|
||||
// Style imports.
|
||||
[
|
||||
"^.+\\.?(css)$"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.json"],
|
||||
"plugins": ["unicorn", "typescript", "oxc", "react", "vitest"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
+10
-21
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "4.4.0",
|
||||
"version": "4.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -37,8 +37,8 @@
|
||||
"postinstall": "pnpm addversion",
|
||||
"dev": "cross-env BROWSER=none vite",
|
||||
"dev:electron": "pnpm dev",
|
||||
"lint": "eslint . --quiet",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "oxlint --quiet --type-aware",
|
||||
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
||||
"build": "vite build",
|
||||
"test": "vitest",
|
||||
"test:pipeline": "vitest run",
|
||||
@@ -57,32 +57,21 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/vite-plugin": "^2.16.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.8.4",
|
||||
"@sentry/vite-plugin": "5.1.1",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@vitejs/plugin-react": "4.5.1",
|
||||
"@vitejs/plugin-react": "5.2.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-utils": "workspace:*",
|
||||
"prettier": "catalog:",
|
||||
"sass": "^1.57.1",
|
||||
"typescript": "catalog:",
|
||||
"vite": "6.3.1",
|
||||
"vite-plugin-compression2": "2.2.0",
|
||||
"vite-plugin-svgr": "4.3.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-compression2": "2.5.1",
|
||||
"vite-plugin-prismjs-plus": "1.1.0",
|
||||
"vite-plugin-svgr": "4.5.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }
|
||||
],
|
||||
"icons": [{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }],
|
||||
"theme_color": "#2B5ABC",
|
||||
"background_color": "#101010",
|
||||
"display": "standalone"
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { Tooltip } from '@base-ui/react/tooltip';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
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 IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
||||
import { AppContextProvider } from './common/context/AppContext';
|
||||
import { ontimeQueryClient } from './common/queryClient';
|
||||
import { connectSocket } from './common/utils/socket';
|
||||
import { baseURI } from './externals';
|
||||
import KeepAwake from './features/keep-awake/KeepAwake';
|
||||
import { TranslationProvider } from './translation/TranslationProvider';
|
||||
import AppRouter from './AppRouter';
|
||||
import { baseURI } from './externals';
|
||||
|
||||
connectSocket();
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { ComponentType, lazy, Suspense, useEffect, useMemo } from 'react';
|
||||
import { Navigate, Route, useLocation, useNavigate, useParams } from 'react-router';
|
||||
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
|
||||
import { 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 { PresetContext } from './common/context/PresetContext';
|
||||
import { useClientPath } from './common/hooks/useClientPath';
|
||||
import useUrlPresets from './common/hooks-query/useUrlPresets';
|
||||
import { useClientPath } from './common/hooks/useClientPath';
|
||||
import { getRouteFromPreset } from './common/utils/urlPresets';
|
||||
import { getIsNavigationLocked, sessionScope } from './externals';
|
||||
import Log from './features/log/Log';
|
||||
import { initializeSentry } from './sentry.config';
|
||||
import Loader from './views/common/loader/Loader';
|
||||
import NotFound from './views/common/not-found/NotFound';
|
||||
import ViewLoader from './views/ViewLoader';
|
||||
import { getIsNavigationLocked, sessionScope } from './externals';
|
||||
import { initializeSentry } from './sentry.config';
|
||||
|
||||
const Timer = lazy(() => import('./views/timer/Timer'));
|
||||
const Countdown = lazy(() => import('./views/countdown/Countdown'));
|
||||
|
||||
@@ -2,16 +2,16 @@ import axios from 'axios';
|
||||
import { TranslationObject } from 'ontime-types';
|
||||
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
|
||||
import { apiEntryUrl, customTranslationsURL, TRANSLATION } from './constants';
|
||||
import { TRANSLATION, apiEntryUrl, customTranslationsURL } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const assetsPath = `${apiEntryUrl}/assets`;
|
||||
|
||||
/**
|
||||
* HTTP request to get css contents
|
||||
*/
|
||||
export async function getCSSContents(): Promise<string> {
|
||||
const res = await axios.get(`${assetsPath}/css`);
|
||||
export async function getCSSContents(options?: RequestOptions): Promise<string> {
|
||||
const res = await axios.get(`${assetsPath}/css`, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ export async function restoreCSSContents(): Promise<string> {
|
||||
/**
|
||||
* HTTP request to get user translation
|
||||
*/
|
||||
export async function getUserTranslation(): Promise<TranslationObject> {
|
||||
const res = await axios.get(customTranslationsURL);
|
||||
export async function getUserTranslation(options?: RequestOptions): Promise<TranslationObject> {
|
||||
const res = await axios.get(customTranslationsURL, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,15 @@ import type {
|
||||
} from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const automationsPath = `${apiEntryUrl}/automations`;
|
||||
|
||||
/**
|
||||
* HTTP request to get the automations settings
|
||||
*/
|
||||
export async function getAutomationSettings(): Promise<AutomationSettings> {
|
||||
const res = await axios.get(automationsPath);
|
||||
export async function getAutomationSettings(options?: RequestOptions): Promise<AutomationSettings> {
|
||||
const res = await axios.get(automationsPath, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
|
||||
axios.defaults.validateStatus = (status) => {
|
||||
return (status >= 200 && status < 300) || status === 304;
|
||||
return status >= 200 && status < 300;
|
||||
};
|
||||
axios.defaults.timeout = axiosConfig.shortTimeout;
|
||||
|
||||
@@ -13,6 +13,7 @@ export const RUNDOWN = ['rundown'];
|
||||
export const RUNTIME = ['runtimeStore'];
|
||||
export const URL_PRESETS = ['urlpresets'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
export const CSS_OVERRIDE = ['cssOverride'];
|
||||
export const CLIENT_LIST = ['clientList'];
|
||||
export const REPORT = ['report'];
|
||||
export const TRANSLATION = ['translation'];
|
||||
@@ -21,9 +22,7 @@ export const TRANSLATION = ['translation'];
|
||||
export const apiEntryUrl = `${serverURL}/data`;
|
||||
|
||||
const userAssetsPath = 'user';
|
||||
const cssOverridePath = 'styles/override.css';
|
||||
const customTranslationsPath = 'translations/translations.json';
|
||||
|
||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||
export const customTranslationsURL = `${serverURL}/${userAssetsPath}/${customTranslationsPath}`;
|
||||
|
||||
@@ -2,14 +2,15 @@ import axios from 'axios';
|
||||
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||
|
||||
/**
|
||||
* Requests list of known custom fields
|
||||
*/
|
||||
export async function getCustomFields(): Promise<CustomFields> {
|
||||
const res = await axios.get(customFieldsPath);
|
||||
export async function getCustomFields(options?: RequestOptions): Promise<CustomFields> {
|
||||
const res = await axios.get(customFieldsPath, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
import { createBlob, downloadBlob } from './utils';
|
||||
|
||||
const dbPath = `${apiEntryUrl}/db`;
|
||||
@@ -10,7 +12,7 @@ const dbPath = `${apiEntryUrl}/db`;
|
||||
* HTTP request to the current DB
|
||||
*/
|
||||
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||
return axios.post(`${dbPath}/download`, { filename });
|
||||
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();
|
||||
formData.append('project', file);
|
||||
const response = await axios.post(`${dbPath}/upload`, formData, {
|
||||
timeout: axiosConfig.longTimeout,
|
||||
headers: {
|
||||
'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
|
||||
*/
|
||||
export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||
const res = await axios.get(`${dbPath}/all`);
|
||||
export async function getProjects(options?: RequestOptions): Promise<ProjectFileListResponse> {
|
||||
const res = await axios.get(`${dbPath}/all`, {
|
||||
signal: options?.signal,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
import { downloadBlob } from './utils';
|
||||
|
||||
const excelPath = `${apiEntryUrl}/excel`;
|
||||
@@ -11,10 +13,12 @@ const excelPath = `${apiEntryUrl}/excel`;
|
||||
* upload Excel file to server
|
||||
* @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();
|
||||
formData.append('excel', file);
|
||||
const response = await axios.post(`${excelPath}/upload`, formData, {
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -27,19 +31,31 @@ type PreviewSpreadsheetResponse = {
|
||||
customFields: CustomFields;
|
||||
summary: RundownSummary;
|
||||
};
|
||||
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||
options,
|
||||
});
|
||||
export async function importRundownPreview(
|
||||
options: ImportMap,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<PreviewSpreadsheetResponse> {
|
||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
|
||||
`${excelPath}/preview`,
|
||||
{
|
||||
options,
|
||||
},
|
||||
{
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a xlsx representation of the rundown from the server
|
||||
*/
|
||||
export async function downloadAsExcel(rundownId: string, fileName?: string) {
|
||||
export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) {
|
||||
try {
|
||||
const response = await axios.get(`${excelPath}/${rundownId}/export`, {
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { apiRepoLatest } from '../../externals';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
export type HasUpdate = {
|
||||
url: string;
|
||||
@@ -10,8 +11,8 @@ export type HasUpdate = {
|
||||
/**
|
||||
* HTTP request to get the latest version and url from github
|
||||
*/
|
||||
export async function getLatestVersion(): Promise<HasUpdate> {
|
||||
const res = await axios.get(apiRepoLatest);
|
||||
export async function getLatestVersion(options?: RequestOptions): Promise<HasUpdate> {
|
||||
const res = await axios.get(apiRepoLatest, { signal: options?.signal });
|
||||
return {
|
||||
url: res.data.html_url as string,
|
||||
version: res.data.tag_name as string,
|
||||
|
||||
@@ -2,14 +2,19 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { ProjectData, ProjectLogoResponse } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
|
||||
const projectPath = `${apiEntryUrl}/project`;
|
||||
|
||||
/**
|
||||
* HTTP request to fetch project data
|
||||
*/
|
||||
export async function getProjectData(): Promise<ProjectData> {
|
||||
const res = await axios.get(projectPath);
|
||||
export async function getProjectData(options?: RequestOptions): Promise<ProjectData> {
|
||||
const res = await axios.get(projectPath, {
|
||||
signal: options?.signal,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -27,6 +32,7 @@ export async function uploadProjectLogo(file: File): Promise<AxiosResponse<Proje
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
const response = await axios.post(`${projectPath}/upload`, formData, {
|
||||
timeout: axiosConfig.longTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
|
||||
@@ -2,16 +2,16 @@ import axios from 'axios';
|
||||
import { OntimeReport } from 'ontime-types';
|
||||
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
|
||||
import { apiEntryUrl, REPORT } from './constants';
|
||||
import { REPORT, apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
export const reportUrl = `${apiEntryUrl}/report`;
|
||||
|
||||
/**
|
||||
* HTTP request to fetch all reports
|
||||
*/
|
||||
export async function fetchReport(): Promise<OntimeReport> {
|
||||
const res = await axios.get(reportUrl);
|
||||
export async function fetchReport(options?: RequestOptions): Promise<OntimeReport> {
|
||||
const res = await axios.get(reportUrl, { signal: options?.signal });
|
||||
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;
|
||||
@@ -2,6 +2,7 @@ import axios, { AxiosResponse } from 'axios';
|
||||
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
type RundownId = string;
|
||||
const rundownPath = `${apiEntryUrl}/rundowns`;
|
||||
@@ -11,16 +12,16 @@ const rundownPath = `${apiEntryUrl}/rundowns`;
|
||||
/**
|
||||
* HTTP request to fetch a list of existing rundowns
|
||||
*/
|
||||
export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
||||
const res = await axios.get(rundownPath);
|
||||
export async function fetchProjectRundownList(options?: RequestOptions): Promise<ProjectRundownsList> {
|
||||
const res = await axios.get(rundownPath, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to fetch all entries in the currently loaded rundown
|
||||
*/
|
||||
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||
const res = await axios.get(`${rundownPath}/current`);
|
||||
export async function fetchCurrentRundown(options?: RequestOptions): Promise<Rundown> {
|
||||
const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal });
|
||||
if (!isValidRundown(res.data)) {
|
||||
throw new Error('Invalid rundown payload');
|
||||
}
|
||||
@@ -152,6 +153,20 @@ export async function postCloneEntry(
|
||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
||||
}
|
||||
|
||||
export type PastePayload = {
|
||||
entryIds: EntryId[];
|
||||
sourceRundownId: string;
|
||||
afterId?: EntryId;
|
||||
beforeId?: EntryId;
|
||||
};
|
||||
|
||||
/**
|
||||
* HTTP request for pasting entries into the active rundown
|
||||
*/
|
||||
export async function postPasteEntries(rundownId: RundownId, data: PastePayload): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/paste`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request for grouping a list of entries into a group
|
||||
*/
|
||||
|
||||
@@ -2,14 +2,15 @@ import axios from 'axios';
|
||||
import { GetInfo, LinkOptions } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const sessionPath = `${apiEntryUrl}/session`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve application info
|
||||
*/
|
||||
export async function getInfo(): Promise<GetInfo> {
|
||||
const res = await axios.get(`${sessionPath}/info`);
|
||||
export async function getInfo(options?: RequestOptions): Promise<GetInfo> {
|
||||
const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { Settings } from 'ontime-types';
|
||||
import { PortInfo, Settings } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const settingsPath = `${apiEntryUrl}/settings`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve application settings
|
||||
*/
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
const res = await axios.get(settingsPath);
|
||||
export async function getSettings(options?: RequestOptions): Promise<Settings> {
|
||||
const res = await axios.get(settingsPath, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -26,3 +27,18 @@ export async function postSettings(data: Settings): Promise<AxiosResponse<Settin
|
||||
export async function postShowWelcomeDialog(show: boolean) {
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ont
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
import { axiosConfig } from './requestTimeouts';
|
||||
|
||||
const sheetsPath = `${apiEntryUrl}/sheets`;
|
||||
|
||||
@@ -23,6 +25,7 @@ export const verifyAuthenticationStatus = async (): Promise<{
|
||||
export const requestConnection = async (
|
||||
file: File,
|
||||
sheetId: string,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<{
|
||||
verification_url: string;
|
||||
user_code: string;
|
||||
@@ -31,6 +34,8 @@ export const requestConnection = async (
|
||||
formData.append('client_secret', file);
|
||||
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -53,24 +58,46 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
|
||||
export const previewRundown = async (
|
||||
sheetId: string,
|
||||
options: ImportMap,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<{
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
summary: RundownSummary;
|
||||
}> => {
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||
const response = await axios.post(
|
||||
`${sheetsPath}/${sheetId}/read`,
|
||||
{ options },
|
||||
{
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getWorksheetNames = async (sheetId: string): Promise<string[]> => {
|
||||
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`);
|
||||
export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise<string[]> => {
|
||||
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`, undefined, {
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* HTTP request to upload the rundown to a google sheet
|
||||
*/
|
||||
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
||||
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options });
|
||||
export const uploadRundown = async (
|
||||
sheetId: string,
|
||||
options: ImportMap,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<void> => {
|
||||
const response = await axios.post(
|
||||
`${sheetsPath}/${sheetId}/write`,
|
||||
{ options },
|
||||
{
|
||||
signal: requestOptions?.signal,
|
||||
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -2,14 +2,15 @@ import axios from 'axios';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve all presets
|
||||
*/
|
||||
export async function getUrlPresets(): Promise<URLPreset[]> {
|
||||
const res = await axios.get(urlPresetsPath);
|
||||
export async function getUrlPresets(options?: RequestOptions): Promise<URLPreset[]> {
|
||||
const res = await axios.get(urlPresetsPath, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from 'axios';
|
||||
import type { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
|
||||
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||
|
||||
@@ -9,8 +10,8 @@ const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||
* HTTP request to fetch view settings
|
||||
* @returns
|
||||
*/
|
||||
export async function getViewSettings(): Promise<ViewSettings> {
|
||||
const res = await axios.get(viewSettingsPath);
|
||||
export async function getViewSettings(options?: RequestOptions): Promise<ViewSettings> {
|
||||
const res = await axios.get(viewSettingsPath, { signal: options?.signal });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
|
||||
import style from './Checkbox.module.scss';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useState } from 'react';
|
||||
import { IoArrowForward } from 'react-icons/io5';
|
||||
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
import Button from '../buttons/Button';
|
||||
import Dialog from '../dialog/Dialog';
|
||||
import Info from '../info/Info';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
import type { ReactNode } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
|
||||
import IconButton from '../buttons/IconButton';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
||||
|
||||
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.
|
||||
import * as Sentry from '@sentry/react';
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
|
||||
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
|
||||
import { runtimeStore } from '../../stores/runtime';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
import { useClientStore } from '../../stores/clientStore';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
// @ts-expect-error no types from library
|
||||
import autosize from 'autosize/dist/autosize';
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useController, UseControllerProps } from 'react-hook-form';
|
||||
import { IoEyedrop } from 'react-icons/io5';
|
||||
import { useDebouncedCallback } from '@mantine/hooks';
|
||||
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 PopoverPicker from '../popover-picker/PopoverPicker';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||
import { InputHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Popover } from '@base-ui/react/popover';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
||||
import { Popover } from '@base-ui/react/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 { getHotkeyHandler, HotkeyItem } from '@mantine/hooks';
|
||||
|
||||
interface UseReactiveTextInputReturn {
|
||||
value: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, TextareaHTMLAttributes } from 'react';
|
||||
import { TextareaHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
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 { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
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 { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
import Input from '../input/Input';
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
outline: none;
|
||||
box-shadow: 0 1px 0 0 currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
top: 10vh;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
|
||||
padding-inline: 1rem;
|
||||
min-width: min(880px, 90vw);
|
||||
min-height: min(200px, 10vh);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
import type { ReactNode } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
|
||||
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 { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { LuCoffee } from 'react-icons/lu';
|
||||
import { useLocation } from 'react-router';
|
||||
import { Dialog } from '@base-ui/react/dialog';
|
||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||
|
||||
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
||||
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
||||
@@ -13,7 +13,6 @@ import { useClientStore } from '../../stores/clientStore';
|
||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
import { RenameClientModal } from '../client-modal/RenameClientModal';
|
||||
|
||||
import ClientLink from './client-link/ClientLink';
|
||||
import EditorNavigation from './editor-navigation/EditorNavigation';
|
||||
import NavigationMenuItem from './navigation-menu-item/NavigationMenuItem';
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useViewParamsEditorStore } from '../view-params-editor/viewParamsEditor.store';
|
||||
|
||||
import FloatingNavigation from './floating-navigation/FloatingNavigation';
|
||||
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
|
||||
import NavigationMenu from './NavigationMenu';
|
||||
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
|
||||
|
||||
interface ViewNavigationMenuProps {
|
||||
/** prevent navigation */
|
||||
|
||||
+3
-1
@@ -24,7 +24,9 @@ export default function NavigationMenuItem({
|
||||
role='button'
|
||||
onClick={onClick}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && onClick();
|
||||
if (isKeyEnter(event)) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
color: $ui-white;
|
||||
background-color: $gray-1250;
|
||||
|
||||
border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
box-shadow: $box-shadow-l1;
|
||||
border: 1px solid $gray-1000;
|
||||
|
||||
@@ -27,4 +27,3 @@
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Popover } from '@base-ui/react/popover';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import style from './Popover.module.scss';
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { PropsWithChildren, useCallback, useContext } from 'react';
|
||||
|
||||
import { isLocalhost } from '../../../externals';
|
||||
import { AppContext } from '../../context/AppContext';
|
||||
|
||||
import PinPage from './PinPage';
|
||||
|
||||
interface ProtectRouteProps {
|
||||
|
||||
@@ -15,6 +15,11 @@ $thumb-color-hover: $white-60;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $blue-500;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties, PropsWithChildren, Ref } from 'react';
|
||||
import { ScrollArea } from '@base-ui/react/scroll-area';
|
||||
import { CSSProperties, PropsWithChildren, Ref } from 'react';
|
||||
|
||||
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 { LuChevronsUpDown } from 'react-icons/lu';
|
||||
import { Select as BaseSelect } from '@base-ui/react/select';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
margin-inline: auto;;
|
||||
margin-inline: auto;
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
max-width: 600px;
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
/* Reset and base styles */
|
||||
appearance: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 99px;
|
||||
background-color: $gray-1100;
|
||||
|
||||
|
||||
/* Transitions */
|
||||
transition: background-color 125ms cubic-bezier(0.26, 0.75, 0.38, 0.45);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.tooltip {
|
||||
font-size: calc(1rem - 3px);
|
||||
background-color: $ui-white;
|
||||
color: $ui-black;
|
||||
color: $ui-black;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 2px;
|
||||
line-height: 1.25em;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import style from './Tooltip.module.scss';
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import Checkbox from '../checkbox/Checkbox';
|
||||
import Input from '../input/input/Input';
|
||||
import Select, { SelectOption } from '../select/Select';
|
||||
import Switch from '../switch/Switch';
|
||||
|
||||
import InlineColourPicker from './InlineColourPicker';
|
||||
import { ParamField } from './viewParams.types';
|
||||
|
||||
@@ -44,7 +43,7 @@ export default function ParamInput({ paramField }: ParamInputProps) {
|
||||
return (
|
||||
<MultiOption
|
||||
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 { IoClose } from 'react-icons/io5';
|
||||
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 { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||
import Button from '../buttons/Button';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
import Info from '../info/Info';
|
||||
|
||||
import { ViewOption } from './viewParams.types';
|
||||
import { getPreservedSearchParams, getURLSearchParamsFromObj } from './viewParams.utils';
|
||||
import { useViewParamsEditorStore } from './viewParamsEditor.store';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useViewUrlPresets } from '../../hooks-query/useUrlPresets';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
@@ -47,4 +47,4 @@
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { IoChevronDown } from 'react-icons/io5';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { IoChevronDown } from 'react-icons/io5';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import { OptionTitle } from './constants';
|
||||
import ParamInput from './ParamInput';
|
||||
import { type ParamField } from './viewParams.types';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CustomFields, ProjectData } from 'ontime-types';
|
||||
|
||||
import type { SelectOption } from '../select/Select';
|
||||
|
||||
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';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||
|
||||
import { useEntryActions } from '../hooks/useEntryAction';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const PresetContext = createContext<URLPreset | undefined>(undefined);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dayInMs } from 'ontime-utils';
|
||||
import { version } from '../../../../../package.json';
|
||||
import { isLocalhost } from '../../externals';
|
||||
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 };
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function useAppVersion() {
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: APP_VERSION,
|
||||
queryFn: getLatestVersion,
|
||||
queryFn: ({ signal }) => getLatestVersion({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
|
||||
@@ -8,12 +8,9 @@ import { automationPlaceholderSettings } from '../models/AutomationSettings';
|
||||
export default function useAutomationSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: AUTOMATION,
|
||||
queryFn: getAutomationSettings,
|
||||
queryFn: ({ signal }) => getAutomationSettings({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch };
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import { getCSSContents } from '../api/assets';
|
||||
import { CSS_OVERRIDE } from '../api/constants';
|
||||
|
||||
export default function useCssOverride(enabled: boolean) {
|
||||
const { data, status } = useQuery({
|
||||
queryKey: CSS_OVERRIDE,
|
||||
queryFn: ({ signal }) => getCSSContents({ signal }),
|
||||
staleTime: MILLIS_PER_HOUR,
|
||||
enabled
|
||||
});
|
||||
|
||||
return {
|
||||
data: data ?? '', status
|
||||
};
|
||||
}
|
||||
@@ -10,12 +10,9 @@ const placeholder: CustomFields = {};
|
||||
export default function useCustomFields() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: CUSTOM_FIELDS,
|
||||
queryFn: getCustomFields,
|
||||
queryFn: ({ signal }) => getCustomFields({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? placeholder, status, isFetching, isError, refetch };
|
||||
|
||||
@@ -8,7 +8,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
export function useCustomTranslation() {
|
||||
const { data, status, refetch } = useQuery({
|
||||
queryKey: TRANSLATION,
|
||||
queryFn: getUserTranslation,
|
||||
queryFn: ({ signal }) => getUserTranslation({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
@@ -9,12 +9,9 @@ import { ontimePlaceholderInfo } from '../models/Info';
|
||||
export default function useInfo() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
|
||||
queryKey: APP_INFO,
|
||||
queryFn: getInfo,
|
||||
queryFn: ({ signal }) => getInfo({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching };
|
||||
|
||||
@@ -8,7 +8,7 @@ import { projectDataPlaceholder } from '../models/ProjectData';
|
||||
export default function useProjectData() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: PROJECT_DATA,
|
||||
queryFn: getProjectData,
|
||||
queryFn: ({ signal }) => getProjectData({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ProjectFile, ProjectFileList, ProjectFileListResponse } from 'ontime-types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { PROJECT_LIST } from '../api/constants';
|
||||
@@ -14,12 +14,9 @@ const placeholderProjectList: ProjectFileListResponse = {
|
||||
function useProjectList() {
|
||||
const { data, status, refetch } = useQuery({
|
||||
queryKey: PROJECT_LIST,
|
||||
queryFn: getProjects,
|
||||
queryFn: ({ signal }) => getProjects({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
return { data: data ?? placeholderProjectList, status, refetch };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@ import { ProjectRundownsList } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { PROJECT_RUNDOWNS } from '../api/constants';
|
||||
import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList, loadRundown, renameRundown } from '../api/rundown';
|
||||
import {
|
||||
createRundown,
|
||||
deleteRundown,
|
||||
duplicateRundown,
|
||||
fetchProjectRundownList,
|
||||
loadRundown,
|
||||
renameRundown,
|
||||
} from '../api/rundown';
|
||||
|
||||
/**
|
||||
* Project rundowns
|
||||
@@ -11,7 +18,7 @@ import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList
|
||||
export function useProjectRundowns() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
|
||||
queryKey: PROJECT_RUNDOWNS,
|
||||
queryFn: fetchProjectRundownList,
|
||||
queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
@@ -30,7 +37,7 @@ export function useMutateProjectRundowns() {
|
||||
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const { mutateAsync: duplicate } = useMutation({
|
||||
mutationFn: duplicateRundown,
|
||||
onMutate: () => {
|
||||
@@ -40,7 +47,7 @@ export function useMutateProjectRundowns() {
|
||||
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const { mutateAsync: rename } = useMutation({
|
||||
mutationFn: ([rundownId, title]: Parameters<typeof renameRundown>) => renameRundown(rundownId, title),
|
||||
onMutate: () => {
|
||||
|
||||
@@ -8,11 +8,8 @@ import { fetchReport } from '../api/report';
|
||||
export default function useReport() {
|
||||
const { data, refetch } = useQuery<OntimeReport>({
|
||||
queryKey: REPORT,
|
||||
queryFn: fetchReport,
|
||||
queryFn: ({ signal }) => fetchReport({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
networkMode: 'always',
|
||||
staleTime: MILLIS_PER_HOUR,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
@@ -24,7 +24,7 @@ const cachedRundownPlaceholder: Rundown = {
|
||||
export default function useRundown() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||
queryKey: RUNDOWN,
|
||||
queryFn: fetchCurrentRundown,
|
||||
queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
||||
export default function useSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: APP_SETTINGS,
|
||||
queryFn: getSettings,
|
||||
queryFn: ({ signal }) => getSettings({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
select: (data) => {
|
||||
const unobfuscated = { ...data };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { URL_PRESETS } from '../api/constants';
|
||||
@@ -13,7 +13,7 @@ interface FetchProps {
|
||||
export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: URL_PRESETS,
|
||||
queryFn: getUrlPresets,
|
||||
queryFn: ({ signal }) => getUrlPresets({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
enabled: !skip,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||
export default function useViewSettings() {
|
||||
const { data, status } = useQuery({
|
||||
queryKey: VIEW_SETTINGS,
|
||||
queryFn: getViewSettings,
|
||||
queryFn: ({ signal }) => getViewSettings({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
staleTime: MILLIS_PER_HOUR,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { MessageTag } from 'ontime-types';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
import { MessageTag } from 'ontime-types';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
import { useClientStore } from '../stores/clientStore';
|
||||
import { sendSocket } from '../utils/socket';
|
||||
|
||||
import { useIsOnline } from './useSocket';
|
||||
|
||||
export const useClientPath = () => {
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
EntryId,
|
||||
InsertOptions,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
MaybeString,
|
||||
OntimeDelay,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
OntimeGroup,
|
||||
OntimeMilestone,
|
||||
PatchWithId,
|
||||
Rundown,
|
||||
SupportedEntry,
|
||||
TimeField,
|
||||
TimeStrategy,
|
||||
TransientEventPayload,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
} 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 { RUNDOWN } from '../api/constants';
|
||||
import {
|
||||
PastePayload,
|
||||
ReorderEntry,
|
||||
deleteEntries,
|
||||
patchReorderEntry,
|
||||
postAddEntry,
|
||||
postCloneEntry,
|
||||
postPasteEntries,
|
||||
putBatchEditEvents,
|
||||
putEditEntry,
|
||||
ReorderEntry,
|
||||
requestApplyDelay,
|
||||
requestDeleteAll,
|
||||
requestEventSwap,
|
||||
@@ -85,9 +103,64 @@ export const useEntryActions = () => {
|
||||
* @private
|
||||
*/
|
||||
const { mutateAsync: addEntryMutation } = useMutation({
|
||||
mutationFn: ([rundownId, entry]: Parameters<typeof postAddEntry>) => postAddEntry(rundownId, entry),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||
mutationFn: ([rundownId, entry]: [string, PatchWithId & InsertOptions]) => postAddEntry(rundownId, entry),
|
||||
onMutate: async ([_rundownId, entry]) => {
|
||||
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 +175,15 @@ export const useEntryActions = () => {
|
||||
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
|
||||
if (isOntimeEvent(newEntry)) {
|
||||
@@ -142,14 +223,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 {
|
||||
await addEntryMutation([rundownId, newEntry]);
|
||||
} catch (error) {
|
||||
@@ -199,6 +272,35 @@ export const useEntryActions = () => {
|
||||
[cloneEntryMutation, getCurrentRundownData],
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to paste entries
|
||||
* @private
|
||||
*/
|
||||
const { mutateAsync: pasteEntriesMutation } = useMutation({
|
||||
mutationFn: ([rundownId, data]: [string, PastePayload]) => postPasteEntries(rundownId, data),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Paste entries from copy store into the active rundown
|
||||
*/
|
||||
const pasteEntries = useCallback(
|
||||
async (data: PastePayload) => {
|
||||
try {
|
||||
const rundownId = getCurrentRundownData()?.id;
|
||||
if (!rundownId) {
|
||||
throw new Error('Rundown not initialised');
|
||||
}
|
||||
|
||||
await pasteEntriesMutation([rundownId, data]);
|
||||
} catch (error) {
|
||||
logAxiosError('Error pasting entries', error);
|
||||
}
|
||||
},
|
||||
[pasteEntriesMutation, getCurrentRundownData],
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to update existing entry
|
||||
* @private
|
||||
@@ -848,6 +950,7 @@ export const useEntryActions = () => {
|
||||
clone,
|
||||
deleteEntry,
|
||||
deleteAllEntries,
|
||||
pasteEntries,
|
||||
ungroup,
|
||||
getEntryById,
|
||||
groupEntries,
|
||||
@@ -864,6 +967,7 @@ export const useEntryActions = () => {
|
||||
clone,
|
||||
deleteEntry,
|
||||
deleteAllEntries,
|
||||
pasteEntries,
|
||||
ungroup,
|
||||
getEntryById,
|
||||
groupEntries,
|
||||
@@ -905,3 +1009,29 @@ function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) {
|
||||
|
||||
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 { RefObject, useCallback, useEffect } from 'react';
|
||||
|
||||
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
|
||||
componentRef: RefObject<ComponentRef>,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user