mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-08 17:03:53 +00:00
Compare commits
312 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 342533e128 | |||
| 5600235ba1 | |||
| a8ea1080f3 | |||
| 36223ff49f | |||
| 3ce5b42e05 | |||
| c6c1e4a5d7 | |||
| 84b73fc02a | |||
| cd8e014f08 | |||
| 2e9ee698e9 | |||
| d06b5af538 | |||
| 24a3823d3b | |||
| 8ad260d28a | |||
| 0726c04f20 | |||
| b66eac3c9f | |||
| 310dde4465 | |||
| aefa4cbf44 | |||
| c8c2a05724 | |||
| fb9fdf6a38 | |||
| 9473880d2c | |||
| 473f50b493 | |||
| 4c08482258 | |||
| c603f29197 | |||
| 063ae69409 | |||
| 6711cdaf4f | |||
| bf8ed8d017 | |||
| d1c58712ae | |||
| e74b2daee7 | |||
| 4872cd0a48 | |||
| 984dcb0181 | |||
| b991627cb7 | |||
| cff73a65a5 | |||
| 269091986a | |||
| 7fdda1c969 | |||
| 97691eba05 | |||
| 3f3cef7d6f | |||
| e0f2830072 | |||
| 3cd6cd2f55 | |||
| 0aeec12ccd | |||
| 56775f621e | |||
| 14a99ce27b | |||
| e090bde8bb | |||
| 629204810f | |||
| fdd81354cc | |||
| 4827b295fa | |||
| a21fdc8481 | |||
| c1a4f04d37 | |||
| 48fe6a980c | |||
| 31d47ed1e5 | |||
| c8fe0bbcba | |||
| c72fff6bb6 | |||
| 36062458fe | |||
| 5c86914cda | |||
| b2b115c329 | |||
| 93977ebc48 | |||
| 5ad69b458d | |||
| 4320ad26a4 | |||
| bab50ea7db | |||
| 5d4fe8e795 | |||
| 7c93f6232d | |||
| 97e3e8f435 | |||
| 44d5859844 | |||
| c522860d28 | |||
| 4e561cf01f | |||
| fb93063b47 | |||
| 24d3bf05ca | |||
| a4b15970de | |||
| 2a5b053d97 | |||
| 02c30c9681 | |||
| 2de7221653 | |||
| 2498e59156 | |||
| f3b4ea0155 | |||
| aebf949883 | |||
| 33ac05ebee | |||
| 421183fe55 | |||
| b66c19769d | |||
| 7b8445597f | |||
| 0b719c485f | |||
| 17deeddb87 | |||
| 2546159a94 | |||
| 2f26f20db5 | |||
| 11e0215530 | |||
| ea54953298 | |||
| d21f132674 | |||
| c7b2f0e89c | |||
| a1505606cf | |||
| 9edff55bd5 | |||
| e287215179 | |||
| 776649c997 | |||
| 9de5e0ac48 | |||
| c7d99073f6 | |||
| 5c1694d802 | |||
| 7cd319a6b3 | |||
| 2c9b6918e6 | |||
| a50cfad7ef | |||
| 66cd1decde | |||
| c9bf6d9812 | |||
| 8557f64382 | |||
| 8e994fb0df | |||
| 5c47a834b5 | |||
| f356afef43 | |||
| eba5252e40 | |||
| 72c4ad789a | |||
| 3118c20719 | |||
| 3754c374c3 | |||
| dd6d74121c | |||
| 6fb42b989e | |||
| 82dc1bc56b | |||
| d2c7b34142 | |||
| 918f8d1b08 | |||
| fcbcf70e01 | |||
| 5c6b06a0d4 | |||
| fc4c1bf22e | |||
| 84fac739f0 | |||
| ef73c87e2f | |||
| 26ee5768a5 | |||
| 27dcf06c73 | |||
| d48b2ae506 | |||
| 14a7c04d3b | |||
| abd9b127db | |||
| 0a80d6db31 | |||
| 977c99e587 | |||
| fe8bbd5003 | |||
| c6e38a2b4a | |||
| 56f18fe71c | |||
| 0b9306de82 | |||
| 3b1e6cf33d | |||
| dda219ebbe | |||
| 8bb771e5c6 | |||
| 8723bcbd33 | |||
| 0dcea4f2d7 | |||
| 696c016c90 | |||
| eed6373dbf | |||
| ac61f257c0 | |||
| 8d8625aa77 | |||
| 1e1aa4bca5 | |||
| 1f0cde6d0d | |||
| c4b66a9ed3 | |||
| 133b3aa819 | |||
| 9566fadbbb | |||
| 62570899e8 | |||
| c1f25f8dba | |||
| 37a91026d8 | |||
| c62ce213f5 | |||
| 529751578d | |||
| dce87c3a81 | |||
| a94b10cb0f | |||
| 6817263123 | |||
| ddd629d7db | |||
| 9d2a3d5a12 | |||
| 4bc271208e | |||
| dbd535dd6c | |||
| 57b7fe214a | |||
| b136e2856a | |||
| 03e68c03a9 | |||
| 44048a889d | |||
| f72db62e08 | |||
| 333f2623a5 | |||
| 3ebb3451e4 | |||
| 73afbacdd7 | |||
| d08452d613 | |||
| 4ebe6ede59 | |||
| 09a32e7612 | |||
| d81f5fcfe1 | |||
| 0a6542df76 | |||
| b2eb140ee2 | |||
| e28f692d3d | |||
| dc73e7e637 | |||
| b81e3a4ba5 | |||
| 6fc7b15d23 | |||
| 99dd4b4d5c | |||
| b60b1beeec | |||
| eab916f410 | |||
| 069a003787 | |||
| 9b08080b7a | |||
| 8e7ab54d39 | |||
| 4a16378786 | |||
| e5b30770ce | |||
| 8465b04c89 | |||
| 718a6ad2b7 | |||
| 6ca4bfc363 | |||
| 4859026989 | |||
| 9167a50eda | |||
| 8adca908b3 | |||
| bf99646ad5 | |||
| 308285ce3e | |||
| dd781a32f6 | |||
| c2a6de6195 | |||
| 0ac2ce8e84 | |||
| 3bdd8424eb | |||
| b3a099255f | |||
| 570ff59cfc | |||
| d0b4c6a279 | |||
| e90161aa33 | |||
| e37254107e | |||
| 9d6bc0a137 | |||
| 4cd2dcbab0 | |||
| 8c593b3487 | |||
| ba74622569 | |||
| f76c0d1a6d | |||
| 8e8d3a7824 | |||
| 6c3ae1f914 | |||
| b6507c27a6 | |||
| 21877e4bff | |||
| b8d7324be0 | |||
| 88b8e83494 | |||
| abce0c498c | |||
| 6b2e45e7de | |||
| 5fb30c75c4 | |||
| 636f78e21e | |||
| 1636745fed | |||
| e01ab0c88b | |||
| 0ede753652 | |||
| 1f6f3df0f2 | |||
| a5afb32b8a | |||
| 9c41f8ff6b | |||
| 1edde03bb4 | |||
| 946b6717d2 | |||
| f99ad83049 | |||
| 665496907e | |||
| 2867c8301d | |||
| 84a5b4fe5b | |||
| 86bb739d90 | |||
| 2c8b6180d7 | |||
| 4cc138e3c0 | |||
| d21e1f996f | |||
| 11c1b472a2 | |||
| 891bda6661 | |||
| c65761448d | |||
| 70558c5104 | |||
| c8bf74a719 | |||
| 10bf4b93ee | |||
| f6449a4326 | |||
| 8d38f52b69 | |||
| 3d40e31730 | |||
| 8ac5c0a9a2 | |||
| 649f0af2cf | |||
| fd9b944ecf | |||
| 0b36608134 | |||
| bac11adeb1 | |||
| 31799f8fab | |||
| af177060ff | |||
| d26a3cd393 | |||
| 38a3165a79 | |||
| 1715c19747 | |||
| 6be6e5fd8d | |||
| 44b0cfd928 | |||
| 3d50b8ce49 | |||
| c3d66f5bd1 | |||
| d08787a6ba | |||
| f0e6231338 | |||
| 430bfb38f2 | |||
| 60354f8ea3 | |||
| bc12531729 | |||
| 0b8a694e4f | |||
| 73ada99596 | |||
| f509655d31 | |||
| fb1bbd6d5c | |||
| 221ff7b3af | |||
| 5d5b48aca6 | |||
| 0f57689750 | |||
| d34280c03d | |||
| bb1e9072fd | |||
| 4503d1e411 | |||
| b4caec0644 | |||
| 960f0322ad | |||
| a12694b186 | |||
| a1d34c8f6c | |||
| 7f8dbbfa87 | |||
| 39e012452b | |||
| c333117347 | |||
| eb0e5e6420 | |||
| 8ba65fa2bd | |||
| efe5ac16f2 | |||
| f4f266dbd4 | |||
| bc9345199b | |||
| 2cc434b0e9 | |||
| 1f71d4578c | |||
| 6c4d80158c | |||
| ab4dd300ca | |||
| 8b84963e17 | |||
| 05e61e7bf8 | |||
| a8fe4c8e68 | |||
| d175c788c5 | |||
| f1f628a48e | |||
| d58335d1a1 | |||
| 78c2bd79b5 | |||
| 28452d1e38 | |||
| 46790441e0 | |||
| 3f35b5e297 | |||
| 49cae647d6 | |||
| 90700c85e8 | |||
| 8fac23c078 | |||
| b5cf6c4bd1 | |||
| 0ea00ff6be | |||
| 30e290c320 | |||
| ac52b3f0c1 | |||
| 8e3db6c559 | |||
| 1d2013ad41 | |||
| 9c910c079d | |||
| 420cef8b0d | |||
| b4c7b84f92 | |||
| 0450eb9169 | |||
| 7fe5223af9 | |||
| ce8d534953 | |||
| 4a6fbc2a92 | |||
| 50ebcf4e14 | |||
| f0f7d1cede | |||
| d2bdb1ea03 | |||
| 9be6febcda | |||
| aa929fd0dc | |||
| 7baa6a4ab9 | |||
| 082fe8b8d7 |
@@ -14,12 +14,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -56,12 +56,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -89,12 +89,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -73,12 +73,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
Generated
-5
@@ -1,5 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="myCustomPropertiesEnabled" value="true" />
|
|
||||||
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
|
||||||
<option name="myCustomPropertiesList">
|
|
||||||
<value>
|
|
||||||
<list size="3">
|
|
||||||
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
|
|
||||||
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
|
|
||||||
<item index="2" class="java.lang.String" itemvalue="app-region" />
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredUrls">
|
|
||||||
<list>
|
|
||||||
<option value="http://localhost" />
|
|
||||||
<option value="http://127.0.0.1" />
|
|
||||||
<option value="http://0.0.0.0" />
|
|
||||||
<option value="http://www.w3.org/" />
|
|
||||||
<option value="http://json-schema.org/draft" />
|
|
||||||
<option value="http://java.sun.com/" />
|
|
||||||
<option value="http://xmlns.jcp.org/" />
|
|
||||||
<option value="http://javafx.com/javafx/" />
|
|
||||||
<option value="http://javafx.com/fxml" />
|
|
||||||
<option value="http://maven.apache.org/xsd/" />
|
|
||||||
<option value="http://maven.apache.org/POM/" />
|
|
||||||
<option value="http://www.springframework.org/schema/" />
|
|
||||||
<option value="http://www.springframework.org/tags" />
|
|
||||||
<option value="http://www.springframework.org/security/tags" />
|
|
||||||
<option value="http://www.thymeleaf.org" />
|
|
||||||
<option value="http://www.jboss.org/j2ee/schema/" />
|
|
||||||
<option value="http://www.jboss.com/xml/ns/" />
|
|
||||||
<option value="http://www.ibm.com/webservices/xsd" />
|
|
||||||
<option value="http://activemq.apache.org/schema/" />
|
|
||||||
<option value="http://schema.cloudfoundry.org/spring/" />
|
|
||||||
<option value="http://schemas.xmlsoap.org/" />
|
|
||||||
<option value="http://cxf.apache.org/schemas/" />
|
|
||||||
<option value="http://primefaces.org/ui" />
|
|
||||||
<option value="http://tiles.apache.org/" />
|
|
||||||
<option value="http://__IP__" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
+2
-2
@@ -9,8 +9,8 @@ Ontime consists of 3 distinct parts
|
|||||||
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__ (~20)
|
- __node__ (~22)
|
||||||
- __pnpm__ (~9)
|
- __pnpm__ (~10)
|
||||||
- __docker__ (only necessary to run and build docker images)
|
- __docker__ (only necessary to run and build docker images)
|
||||||
|
|
||||||
## LOCAL DEVELOPMENT
|
## LOCAL DEVELOPMENT
|
||||||
|
|||||||
+4
-3
@@ -1,13 +1,13 @@
|
|||||||
FROM node:20-bullseye AS builder
|
FROM node:22-bullseye AS builder
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN npm install -g pnpm@9.5.0
|
RUN npm install -g pnpm@10.11.0
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
# Environment Variable to signal that we are running production
|
# Environment Variable to signal that we are running production
|
||||||
@@ -24,6 +24,7 @@ COPY --from=builder /app/apps/client/build ./client/
|
|||||||
COPY --from=builder /app/apps/server/dist/ ./server/
|
COPY --from=builder /app/apps/server/dist/ ./server/
|
||||||
COPY --from=builder /app/apps/server/src/external/ ./external/
|
COPY --from=builder /app/apps/server/src/external/ ./external/
|
||||||
COPY --from=builder /app/apps/server/src/user/ ./user/
|
COPY --from=builder /app/apps/server/src/user/ ./user/
|
||||||
|
COPY --from=builder /app/apps/server/src/html/ ./html/
|
||||||
|
|
||||||
# Export default ports
|
# Export default ports
|
||||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
[](https://github.com/sponsors/cpvalente)
|
[](https://github.com/sponsors/cpvalente)
|
||||||
[](https://www.buymeacoffee.com/cpvalente)
|
[](https://www.buymeacoffee.com/cpvalente)
|
||||||
|
|
||||||
@@ -12,7 +13,11 @@
|
|||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">MacOS Arm</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">MacOS Arm</a>
|
||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">MacOS Intel</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">MacOS Intel</a>
|
||||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage">Linux</a>
|
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage">Linux</a>
|
||||||
|
|
||||||
|
... or
|
||||||
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
||||||
|
- Install from <a href="https://www.npmjs.com/package/ontime">NPM</a>
|
||||||
|
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
||||||
|
|
||||||
## Need help?
|
## 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.
|
||||||
@@ -98,19 +103,12 @@ IP.ADDRESS:4001/studio > Studio Clock
|
|||||||
IP.ADDRESS:4001/timeline > Timeline
|
IP.ADDRESS:4001/timeline > Timeline
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
|
||||||
For the public views
|
|
||||||
-------------------------------------------------------------
|
|
||||||
IP.ADDRESS:4001/public > Public / Foyer view
|
|
||||||
IP.ADDRESS:4001/lower > Lower Thirds
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
```
|
||||||
For production views
|
For production views
|
||||||
-------------------------------------------------------------
|
-------------------------------------------------------------
|
||||||
IP.ADDRESS:4001/editor > the control interface, same as the app
|
IP.ADDRESS:4001/editor > the control interface, same as the app
|
||||||
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
||||||
IP.ADDRESS:4001/operator > automated views for operators
|
IP.ADDRESS:4001/op > automated views for operators
|
||||||
```
|
```
|
||||||
|
|
||||||
More information is available [in our docs](https://docs.getontime.no)
|
More information is available [in our docs](https://docs.getontime.no)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.10.3",
|
"version": "3.15.2",
|
||||||
"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",
|
||||||
|
|||||||
@@ -15,11 +15,12 @@
|
|||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react",
|
"react",
|
||||||
"testing-library",
|
"react-compiler",
|
||||||
"simple-import-sort",
|
"simple-import-sort",
|
||||||
"@tanstack/query"
|
"@tanstack/query"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"react-compiler/react-compiler": "warn",
|
||||||
"react/jsx-no-bind": [
|
"react/jsx-no-bind": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|||||||
+21
-21
@@ -1,36 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.10.3",
|
"version": "3.15.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui-components/react": "1.0.0-beta.1",
|
||||||
"@chakra-ui/react": "^2.7.0",
|
"@chakra-ui/react": "^2.7.0",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@fontsource/open-sans": "^5.0.28",
|
"@fontsource/open-sans": "^5.0.28",
|
||||||
"@mantine/hooks": "^7.13.3",
|
"@mantine/hooks": "^8.1.2",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
|
||||||
"@sentry/react": "^8.43.0",
|
"@sentry/react": "^8.43.0",
|
||||||
|
"@table-nav/react": "^0.0.7",
|
||||||
"@tanstack/react-query": "^5.62.7",
|
"@tanstack/react-query": "^5.62.7",
|
||||||
"@tanstack/react-query-devtools": "^5.62.7",
|
"@tanstack/react-query-devtools": "^5.62.7",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.9.0",
|
||||||
"color": "^4.2.3",
|
|
||||||
"csv-stringify": "^6.4.5",
|
"csv-stringify": "^6.4.5",
|
||||||
"framer-motion": "^10.10.0",
|
"framer-motion": "^10.10.0",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-fast-compare": "^3.2.2",
|
"react-fast-compare": "^3.2.2",
|
||||||
"react-hook-form": "^7.53.1",
|
"react-hook-form": "^7.53.1",
|
||||||
|
"react-icons": "5.4.0",
|
||||||
"react-qr-code": "^2.0.12",
|
"react-qr-code": "^2.0.12",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"web-vitals": "^3.1.1",
|
"web-vitals": "^3.1.1",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
@@ -62,34 +66,30 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/vite-plugin": "^2.16.1",
|
"@sentry/vite-plugin": "^2.16.1",
|
||||||
"@tanstack/eslint-plugin-query": "^5.8.4",
|
"@tanstack/eslint-plugin-query": "^5.8.4",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@testing-library/react": "^13.1.1",
|
|
||||||
"@testing-library/user-event": "^14.1.1",
|
|
||||||
"@types/color": "^3.0.3",
|
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/testing-library__jest-dom": "^5.14.5",
|
|
||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"@typescript-eslint/parser": "catalog:",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "4.5.1",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"eslint-config-prettier": "catalog:",
|
"eslint-config-prettier": "catalog:",
|
||||||
"eslint-plugin-jest": "^28.6.0",
|
"eslint-plugin-jest": "^28.6.0",
|
||||||
"eslint-plugin-prettier": "catalog:",
|
"eslint-plugin-prettier": "catalog:",
|
||||||
"eslint-plugin-react": "^7.32.0",
|
"eslint-plugin-react": "^7.32.0",
|
||||||
|
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"eslint-plugin-testing-library": "^5.9.1",
|
"happy-dom": "^16.7.2",
|
||||||
"jsdom": "^21.1.0",
|
|
||||||
"ontime-types": "workspace:*",
|
"ontime-types": "workspace:*",
|
||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "^5.2.11",
|
"vite": "6.3.1",
|
||||||
"vite-plugin-compression2": "^1.3.3",
|
"vite-plugin-compression2": "1.4.0",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "4.3.0",
|
||||||
"vite-tsconfig-paths": "^4.3.1",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,19 @@ import ViewLoader from './views/ViewLoader';
|
|||||||
import { ONTIME_VERSION } from './ONTIME_VERSION';
|
import { ONTIME_VERSION } from './ONTIME_VERSION';
|
||||||
import { sentryDsn, sentryRecommendedIgnore } from './sentry.config';
|
import { sentryDsn, sentryRecommendedIgnore } from './sentry.config';
|
||||||
|
|
||||||
const Editor = React.lazy(() => import('./features/editors/ProtectedEditor'));
|
const Editor = React.lazy(() => import('./views/editor/ProtectedEditor'));
|
||||||
const Cuesheet = React.lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
|
const Cuesheet = React.lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
|
||||||
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
|
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
|
||||||
|
|
||||||
const TimerView = React.lazy(() => import('./features/viewers/timer/Timer'));
|
const TimerView = React.lazy(() => import('./views/timer/Timer'));
|
||||||
const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
|
const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
|
||||||
const ClockView = React.lazy(() => import('./features/viewers/clock/Clock'));
|
const ClockView = React.lazy(() => import('./features/viewers/clock/Clock'));
|
||||||
const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdown'));
|
const Countdown = React.lazy(() => import('./views/countdown/Countdown'));
|
||||||
|
|
||||||
const Backstage = React.lazy(() => import('./features/viewers/backstage/Backstage'));
|
const Backstage = React.lazy(() => import('./views/backstage/Backstage'));
|
||||||
const Timeline = React.lazy(() => import('./views/timeline/TimelinePage'));
|
const Timeline = React.lazy(() => import('./views/timeline/TimelinePage'));
|
||||||
const Public = React.lazy(() => import('./features/viewers/public/Public'));
|
|
||||||
const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
|
const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
|
||||||
const StudioClock = React.lazy(() => import('./features/viewers/studio/StudioClock'));
|
const StudioClock = React.lazy(() => import('./views/studio/Studio'));
|
||||||
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
|
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
|
||||||
|
|
||||||
const STimer = withPreset(withData(TimerView));
|
const STimer = withPreset(withData(TimerView));
|
||||||
@@ -40,10 +39,11 @@ const SClock = withPreset(withData(ClockView));
|
|||||||
const SCountdown = withPreset(withData(Countdown));
|
const SCountdown = withPreset(withData(Countdown));
|
||||||
const SBackstage = withPreset(withData(Backstage));
|
const SBackstage = withPreset(withData(Backstage));
|
||||||
const SProjectInfo = withPreset(withData(ProjectInfo));
|
const SProjectInfo = withPreset(withData(ProjectInfo));
|
||||||
const SPublic = withPreset(withData(Public));
|
|
||||||
const SLowerThird = withPreset(withData(Lower));
|
const SLowerThird = withPreset(withData(Lower));
|
||||||
const SStudio = withPreset(withData(StudioClock));
|
const SStudio = withPreset(withData(StudioClock));
|
||||||
const STimeline = withPreset(withData(Timeline));
|
const STimeline = withPreset(withData(Timeline));
|
||||||
|
const PCuesheet = withPreset(Cuesheet);
|
||||||
|
const POperator = withPreset(Operator);
|
||||||
|
|
||||||
const EditorFeatureWrapper = React.lazy(() => import('./features/EditorFeatureWrapper'));
|
const EditorFeatureWrapper = React.lazy(() => import('./features/EditorFeatureWrapper'));
|
||||||
const RundownPanel = React.lazy(() => import('./features/rundown/RundownExport'));
|
const RundownPanel = React.lazy(() => import('./features/rundown/RundownExport'));
|
||||||
@@ -86,14 +86,6 @@ export default function AppRouter() {
|
|||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path='/public'
|
|
||||||
element={
|
|
||||||
<ViewLoader>
|
|
||||||
<SPublic />
|
|
||||||
</ViewLoader>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path='/minimal'
|
path='/minimal'
|
||||||
element={
|
element={
|
||||||
@@ -155,8 +147,15 @@ export default function AppRouter() {
|
|||||||
|
|
||||||
{/*/!* Protected Routes *!/*/}
|
{/*/!* Protected Routes *!/*/}
|
||||||
<Route path='/editor' element={<Editor />} />
|
<Route path='/editor' element={<Editor />} />
|
||||||
<Route path='/cuesheet' element={<Cuesheet />} />
|
<Route path='/cuesheet' element={<PCuesheet />} />
|
||||||
<Route path='/op' element={<Operator />} />
|
<Route
|
||||||
|
path='/op'
|
||||||
|
element={
|
||||||
|
<ViewLoader>
|
||||||
|
<POperator />
|
||||||
|
</ViewLoader>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*/!* Protected Routes - Elements *!/*/}
|
{/*/!* Protected Routes - Elements *!/*/}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const assetsPath = `${apiEntryUrl}/assets`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to get css contents
|
||||||
|
*/
|
||||||
|
export async function getCSSContents(): Promise<string> {
|
||||||
|
const res = await axios.get(`${assetsPath}/css`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to post css contents
|
||||||
|
*/
|
||||||
|
export async function postCSSContents(css: string): Promise<void> {
|
||||||
|
await axios.post(`${assetsPath}/css`, {
|
||||||
|
css,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to restore default css
|
||||||
|
*/
|
||||||
|
export async function restoreCSSContents(): Promise<string> {
|
||||||
|
const res = await axios.post(`${assetsPath}/css/restore`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type {
|
||||||
|
Automation,
|
||||||
|
AutomationDTO,
|
||||||
|
AutomationOutput,
|
||||||
|
AutomationSettings,
|
||||||
|
Trigger,
|
||||||
|
TriggerDTO,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const automationsPath = `${apiEntryUrl}/automations`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to get the automations settings
|
||||||
|
*/
|
||||||
|
export async function getAutomationSettings(): Promise<AutomationSettings> {
|
||||||
|
const res = await axios.get(automationsPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to edit the automations settings
|
||||||
|
*/
|
||||||
|
export async function editAutomationSettings(
|
||||||
|
automationSettings: Partial<AutomationSettings>,
|
||||||
|
): Promise<AutomationSettings> {
|
||||||
|
const res = await axios.post(automationsPath, automationSettings);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to create a new automation trigger
|
||||||
|
*/
|
||||||
|
export async function addTrigger(trigger: TriggerDTO): Promise<Trigger> {
|
||||||
|
const res = await axios.post(`${automationsPath}/trigger`, trigger);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to update an automation trigger
|
||||||
|
*/
|
||||||
|
export async function editTrigger(id: string, trigger: Trigger): Promise<Trigger> {
|
||||||
|
const res = await axios.put(`${automationsPath}/trigger/${id}`, trigger);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete an automation trigger
|
||||||
|
*/
|
||||||
|
export function deleteTrigger(id: string): Promise<void> {
|
||||||
|
return axios.delete(`${automationsPath}/trigger/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to create a new automation
|
||||||
|
*/
|
||||||
|
export async function addAutomation(automation: AutomationDTO): Promise<Automation> {
|
||||||
|
const res = await axios.post(`${automationsPath}/automation`, automation);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to update a automation
|
||||||
|
*/
|
||||||
|
export async function editAutomation(id: string, automation: Automation): Promise<Automation> {
|
||||||
|
const res = await axios.put(`${automationsPath}/automation/${id}`, automation);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete a automation
|
||||||
|
*/
|
||||||
|
export function deleteAutomation(id: string): Promise<void> {
|
||||||
|
return axios.delete(`${automationsPath}/automation/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to test automation output
|
||||||
|
* The return is irrelevant as we care for the resolution of the promise
|
||||||
|
*/
|
||||||
|
export function testOutput(output: AutomationOutput): Promise<void> {
|
||||||
|
return axios.post(`${automationsPath}/test`, output);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
axios.defaults.validateStatus = (status) => {
|
||||||
|
return (status >= 200 && status < 300) || status === 304;
|
||||||
|
};
|
||||||
@@ -4,26 +4,23 @@ import { serverURL } from '../../externals';
|
|||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
export const APP_VERSION = ['appVersion'];
|
export const APP_VERSION = ['appVersion'];
|
||||||
|
export const AUTOMATION = ['automation'];
|
||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
export const HTTP_SETTINGS = ['httpSettings'];
|
|
||||||
export const OSC_SETTINGS = ['oscSettings'];
|
|
||||||
export const PROJECT_DATA = ['project'];
|
export const PROJECT_DATA = ['project'];
|
||||||
export const PROJECT_LIST = ['projectList'];
|
export const PROJECT_LIST = ['projectList'];
|
||||||
|
export const PROJECT_RUNDOWNS = ['projectRundowns'];
|
||||||
export const RUNDOWN = ['rundown'];
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
export const SHEET_STATE = ['sheetState'];
|
|
||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
export const CLIENT_LIST = ['clientList'];
|
export const CLIENT_LIST = ['clientList'];
|
||||||
|
export const REPORT = ['report'];
|
||||||
|
|
||||||
// API URLs
|
// API URLs
|
||||||
export const apiEntryUrl = `${serverURL}/data`;
|
export const apiEntryUrl = `${serverURL}/data`;
|
||||||
|
|
||||||
export const projectDataURL = `${serverURL}/project`;
|
const userAssetsPath = 'user';
|
||||||
export const rundownURL = `${serverURL}/events`;
|
const cssOverridePath = 'styles/override.css';
|
||||||
export const ontimeURL = `${serverURL}/ontime`;
|
|
||||||
|
|
||||||
export const userAssetsPath = 'user';
|
|
||||||
export const cssOverridePath = 'styles/override.css';
|
|
||||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
||||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||||
|
|||||||
@@ -1,38 +1,45 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { CustomField, CustomFieldLabel, CustomFields } from 'ontime-types';
|
import { CustomField, CustomFieldKey } from 'ontime-types'; // Removed CustomFields
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
// Define CustomFieldWithKey for client-side usage
|
||||||
|
export type CustomFieldWithKey = CustomField & { key: CustomFieldKey };
|
||||||
|
|
||||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests list of known custom fields
|
* Requests list of known custom fields, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function getCustomFields(): Promise<CustomFields> {
|
export async function getCustomFields(): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.get(customFieldsPath);
|
const res = await axios.get<CustomFieldWithKey[]>(customFieldsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets list of known custom fields
|
* Sets list of known custom fields
|
||||||
|
* Returns the updated list, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
export async function postCustomField(newField: CustomField): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.post(customFieldsPath, { ...newField });
|
const res = await axios.post<CustomFieldWithKey[]>(customFieldsPath, { ...newField });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits single custom field
|
* Edits single custom field
|
||||||
|
* Returns the updated list, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
export async function editCustomField(key: CustomFieldKey, newField: Partial<CustomField>): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.put(`${customFieldsPath}/${label}`, { ...newField });
|
// Ensure newField can include 'order' by using Partial<CustomField>
|
||||||
|
const res = await axios.put<CustomFieldWithKey[]>(`${customFieldsPath}/${key}`, { ...newField });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes single custom field
|
* Deletes single custom field
|
||||||
|
* Returns the updated list, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.delete(`${customFieldsPath}/${label}`);
|
const res = await axios.delete<CustomFieldWithKey[]>(`${customFieldsPath}/${key}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
import { makeCSV, makeTable } from '../../views/cuesheet/cuesheet.utils';
|
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||||
|
import { aggregateRundowns, makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import { createBlob, downloadBlob } from './utils';
|
import { createBlob, downloadBlob } from './utils';
|
||||||
@@ -39,10 +40,12 @@ export async function downloadProject(fileName: string) {
|
|||||||
export async function downloadCSV(fileName: string = 'rundown') {
|
export async function downloadCSV(fileName: string = 'rundown') {
|
||||||
try {
|
try {
|
||||||
const { data, name } = await fileDownload(fileName);
|
const { data, name } = await fileDownload(fileName);
|
||||||
const { project, rundown, customFields } = data;
|
const { project, rundowns, customFields } = data;
|
||||||
|
|
||||||
const sheetData = makeTable(project, rundown, customFields);
|
const flatRundowns = aggregateRundowns(rundowns);
|
||||||
const fileContent = makeCSV(sheetData);
|
const sheetData = makeTable(project, flatRundowns, customFields);
|
||||||
|
|
||||||
|
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
||||||
|
|
||||||
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
||||||
downloadBlob(blob, `${name}.csv`);
|
downloadBlob(blob, `${name}.csv`);
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
import { CustomFields, Rundown } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
const excelPath = `${apiEntryUrl}/excel`;
|
const excelPath = `${apiEntryUrl}/excel`;
|
||||||
|
|
||||||
type PreviewSpreadsheetResponse = {
|
|
||||||
rundown: OntimeRundown;
|
|
||||||
customFields: CustomFields;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* upload Excel file to server
|
* upload Excel file to server
|
||||||
* @return string - file ID op the uploaded file
|
* @return string - file ID op the uploaded file
|
||||||
@@ -34,6 +29,10 @@ export async function getWorksheetNames(): Promise<string[]> {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreviewSpreadsheetResponse = {
|
||||||
|
rundown: Rundown;
|
||||||
|
customFields: CustomFields;
|
||||||
|
};
|
||||||
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
||||||
options,
|
options,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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(): Promise<HasUpdate> {
|
||||||
const res = await axios.get(`${apiRepoLatest}`);
|
const res = await axios.get(apiRepoLatest);
|
||||||
return {
|
return {
|
||||||
url: res.data.html_url as string,
|
url: res.data.html_url as string,
|
||||||
version: res.data.tag_name as string,
|
version: res.data.tag_name as string,
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
|
||||||
import { HttpSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
|
||||||
|
|
||||||
const httpPath = `${apiEntryUrl}/http`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to retrieve http settings
|
|
||||||
*/
|
|
||||||
export async function getHTTP(): Promise<HttpSettings> {
|
|
||||||
const res = await axios.get(httpPath);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to mutate http settings
|
|
||||||
*/
|
|
||||||
export async function postHTTP(data: HttpSettings): Promise<AxiosResponse<HttpSettings>> {
|
|
||||||
return axios.post(httpPath, data);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
|
||||||
import { OSCSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
|
||||||
|
|
||||||
const oscPath = `${apiEntryUrl}/osc`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to retrieve osc settings
|
|
||||||
*/
|
|
||||||
export async function getOSC(): Promise<OSCSettings> {
|
|
||||||
const res = await axios.get(oscPath);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to mutate osc settings
|
|
||||||
*/
|
|
||||||
export async function postOSC(data: OSCSettings): Promise<AxiosResponse<OSCSettings>> {
|
|
||||||
return axios.post(oscPath, data);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { OntimeReport } from 'ontime-types';
|
||||||
|
|
||||||
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
|
||||||
|
import { apiEntryUrl, REPORT } from './constants';
|
||||||
|
|
||||||
|
export const reportUrl = `${apiEntryUrl}/report`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to fetch all reports
|
||||||
|
*/
|
||||||
|
export async function fetchReport(): Promise<OntimeReport> {
|
||||||
|
const res = await axios.get(reportUrl);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteReport(id: string) {
|
||||||
|
await axios.delete(`${reportUrl}/${id}`);
|
||||||
|
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAllReport() {
|
||||||
|
await axios.delete(`${reportUrl}/all`);
|
||||||
|
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||||
|
}
|
||||||
@@ -1,29 +1,45 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
import {
|
||||||
|
EntryId,
|
||||||
|
MessageResponse,
|
||||||
|
OntimeEntry,
|
||||||
|
OntimeEvent,
|
||||||
|
ProjectRundownsList,
|
||||||
|
Rundown,
|
||||||
|
TransientEventPayload,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
const rundownPath = `${apiEntryUrl}/rundown`;
|
const rundownPath = `${apiEntryUrl}/rundown`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch all events
|
* HTTP request to fetch a list of existing rundowns
|
||||||
*/
|
*/
|
||||||
export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
||||||
const res = await axios.get(`${rundownPath}/normalised`);
|
const res = await axios.get(rundownPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to post new event
|
* HTTP request to fetch all events
|
||||||
*/
|
*/
|
||||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||||
|
const res = await axios.get(`${rundownPath}/current`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to post new entry
|
||||||
|
*/
|
||||||
|
export async function postAddEntry(data: TransientEventPayload): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
return axios.post(rundownPath, data);
|
return axios.post(rundownPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to put new event
|
* HTTP request to edit an entry
|
||||||
*/
|
*/
|
||||||
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function putEditEntry(data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
return axios.put(rundownPath, data);
|
return axios.put(rundownPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,22 +49,22 @@ type BatchEditEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to put multiple events
|
* HTTP request to edit multiple events
|
||||||
*/
|
*/
|
||||||
export async function requestBatchPutEvents(data: BatchEditEntry): Promise<AxiosResponse<MessageResponse>> {
|
export async function putBatchEditEvents(data: BatchEditEntry): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.put(`${rundownPath}/batch`, data);
|
return axios.put(`${rundownPath}/batch`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReorderEntry = {
|
export type ReorderEntry = {
|
||||||
eventId: string;
|
entryId: EntryId;
|
||||||
from: number;
|
destinationId: EntryId;
|
||||||
to: number;
|
order: 'before' | 'after' | 'insert';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to reorder events
|
* HTTP request to reorder an entry
|
||||||
*/
|
*/
|
||||||
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function patchReorderEntry(data: ReorderEntry): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.patch(`${rundownPath}/reorder`, data);
|
return axios.patch(`${rundownPath}/reorder`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,15 +83,36 @@ export async function requestEventSwap(data: SwapEntry): Promise<AxiosResponse<M
|
|||||||
/**
|
/**
|
||||||
* HTTP request to request application of delay
|
* HTTP request to request application of delay
|
||||||
*/
|
*/
|
||||||
export async function requestApplyDelay(eventId: string): Promise<AxiosResponse<MessageResponse>> {
|
export async function requestApplyDelay(delayId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.patch(`${rundownPath}/applydelay/${eventId}`);
|
return axios.patch(`${rundownPath}/applydelay/${delayId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to delete given event
|
* HTTP request for cloning an entry
|
||||||
*/
|
*/
|
||||||
export async function requestDelete(eventIds: string[]): Promise<AxiosResponse<MessageResponse>> {
|
export async function postCloneEntry(entryId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.delete(rundownPath, { data: { ids: eventIds } });
|
return axios.post(`${rundownPath}/clone/${entryId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request for dissolving of a block
|
||||||
|
*/
|
||||||
|
export async function requestUngroup(blockId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/ungroup/${blockId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request for grouping a list of entries into a block
|
||||||
|
*/
|
||||||
|
export async function requestGroupEntries(entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/group`, { ids: entryIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete entries
|
||||||
|
*/
|
||||||
|
export async function deleteEntries(entryIds: EntryId[]): Promise<AxiosResponse<MessageResponse>> {
|
||||||
|
return axios.delete(rundownPath, { data: { ids: entryIds } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,3 +12,16 @@ export async function getInfo(): Promise<GetInfo> {
|
|||||||
const res = await axios.get(`${sessionPath}/info`);
|
const res = await axios.get(`${sessionPath}/info`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to get a pre-authenticated URL
|
||||||
|
*/
|
||||||
|
export async function generateUrl(
|
||||||
|
baseUrl: string,
|
||||||
|
path: string,
|
||||||
|
lock: boolean,
|
||||||
|
authenticate: boolean,
|
||||||
|
): Promise<string> {
|
||||||
|
const res = await axios.post(`${sessionPath}/url`, { baseUrl, path, lock, authenticate });
|
||||||
|
return res.data.url;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,3 +19,10 @@ export async function getSettings(): Promise<Settings> {
|
|||||||
export async function postSettings(data: Settings): Promise<AxiosResponse<Settings>> {
|
export async function postSettings(data: Settings): Promise<AxiosResponse<Settings>> {
|
||||||
return axios.post(settingsPath, data);
|
return axios.post(settingsPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows setting the welcome modal dialog state from the clients
|
||||||
|
*/
|
||||||
|
export async function postShowWelcomeDialog(show: boolean) {
|
||||||
|
axios.post(`${settingsPath}/welcomedialog`, { show });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
@@ -54,7 +54,7 @@ export const previewRundown = async (
|
|||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ImportMap,
|
options: ImportMap,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
rundown: OntimeRundown;
|
rundown: Rundown;
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
}> => {
|
}> => {
|
||||||
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { addLog } from '../stores/logger';
|
|||||||
import { nowInMillis } from '../utils/time';
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility unrwap a potential axios error
|
* Utility unwrap a potential axios error
|
||||||
* @param error
|
* @param error
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@@ -34,7 +34,7 @@ export function maybeAxiosError(error: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility unrwaps a potential axios error and sends to logger
|
* Utility unwraps a potential axios error and sends to logger
|
||||||
* @param prepend
|
* @param prepend
|
||||||
* @param error
|
* @param error
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import type { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
const viewSettingsPath = apiEntryUrl + '/view-settings';
|
||||||
|
|
||||||
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
export async function getViewSettings() {
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to retrieve view settings
|
|
||||||
*/
|
|
||||||
export async function getView(): Promise<ViewSettings> {
|
|
||||||
const res = await axios.get(viewSettingsPath);
|
const res = await axios.get(viewSettingsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP request to mutate view settings
|
|
||||||
*/
|
|
||||||
export async function postViewSettings(data: ViewSettings) {
|
export async function postViewSettings(data: ViewSettings) {
|
||||||
return axios.post(viewSettingsPath, data);
|
const res = await axios.post(viewSettingsPath, data);
|
||||||
|
return res.data as ViewSettings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
@use "../../../theme/mixins" as *;
|
|
||||||
|
|
||||||
.link {
|
|
||||||
@include action-link;
|
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkIcon {
|
|
||||||
margin-left: $element-inner-spacing;
|
|
||||||
display: inline-block;
|
|
||||||
@include rotate-fourty-five;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { MouseEvent, ReactNode } from 'react';
|
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|
||||||
|
|
||||||
import { openLink } from '../../utils/linkUtils';
|
|
||||||
|
|
||||||
import style from './AppLink.module.scss';
|
|
||||||
|
|
||||||
interface AppLinkProps {
|
|
||||||
href: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppLink(props: AppLinkProps) {
|
|
||||||
const { href, children } = props;
|
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
openLink(href);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a href='#!' target='_blank' rel='noreferrer' className={style.link} onClick={handleClick}>
|
|
||||||
{children} <IoArrowUp className={style.linkIcon} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.subtle {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $blue-400;
|
||||||
|
line-height: 1em;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-white {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: $gray-1050;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: $blue-700;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $blue-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $blue-800;
|
||||||
|
border-color: $blue-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $viewer-opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.destructive {
|
||||||
|
background: $red-700;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $red-600;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $red-800;
|
||||||
|
border-color: $red-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-destructive {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $red-400;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted {
|
||||||
|
background: transparent;
|
||||||
|
color: $blue-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted-white {
|
||||||
|
background: transparent;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ghosted-destructive {
|
||||||
|
background: transparent;
|
||||||
|
color: $red-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
@import './BaseButtonStyles.module.scss';
|
||||||
|
|
||||||
|
.baseButton {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
padding-inline: 1em;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $blue-500;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
cursor: wait;
|
||||||
|
.content {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingOverlay {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
stroke-dasharray: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
height: 2rem;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xlarge {
|
||||||
|
height: 3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||||
|
import { IoEllipseOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './Button.module.scss';
|
||||||
|
|
||||||
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?:
|
||||||
|
| 'primary'
|
||||||
|
| 'subtle'
|
||||||
|
| 'subtle-white'
|
||||||
|
| 'destructive'
|
||||||
|
| 'subtle-destructive'
|
||||||
|
| 'ghosted'
|
||||||
|
| 'ghosted-white'
|
||||||
|
| 'ghosted-destructive';
|
||||||
|
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
|
fluid?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, children, variant = 'subtle', size = 'medium', fluid, loading, ...buttonProps }, ref) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={cx([
|
||||||
|
style.baseButton,
|
||||||
|
style[variant],
|
||||||
|
style[size],
|
||||||
|
fluid && style.fluid,
|
||||||
|
loading && style.loading,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
type='button'
|
||||||
|
disabled={loading || buttonProps.disabled}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<span className={style.content}>{children}</span>
|
||||||
|
{loading && (
|
||||||
|
<div className={style.loadingOverlay}>
|
||||||
|
<IoEllipseOutline className={style.spinner} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
@import './BaseButtonStyles.module.scss';
|
||||||
|
|
||||||
|
.baseIconButton {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xlarge {
|
||||||
|
height: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { ButtonHTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './IconButton.module.scss';
|
||||||
|
|
||||||
|
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?:
|
||||||
|
| 'primary'
|
||||||
|
| 'subtle'
|
||||||
|
| 'subtle-white'
|
||||||
|
| 'destructive'
|
||||||
|
| 'subtle-destructive'
|
||||||
|
| 'ghosted'
|
||||||
|
| 'ghosted-white'
|
||||||
|
| 'ghosted-destructive';
|
||||||
|
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IconButton({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
variant = 'subtle',
|
||||||
|
size = 'medium',
|
||||||
|
...buttonProps
|
||||||
|
}: IconButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cx([style.baseIconButton, style[variant], style[size], className])}
|
||||||
|
type='button'
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.checkbox {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: $component-border-radius-sm;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
background-color: $gray-1100;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $gray-1000;
|
||||||
|
}
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-checked] {
|
||||||
|
background-color: $blue-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $blue-400;
|
||||||
|
}
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $blue-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $blue-500;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
display: flex;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
|
import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
|
||||||
|
|
||||||
|
import style from './Checkbox.module.scss';
|
||||||
|
|
||||||
|
export default function Checkbox(checkboxProps: BaseCheckbox.Root.Props) {
|
||||||
|
return (
|
||||||
|
<BaseCheckbox.Root className={style.checkbox} {...checkboxProps}>
|
||||||
|
<BaseCheckbox.Indicator className={style.indicator}>
|
||||||
|
<IoCheckmark className={style.icon} />
|
||||||
|
</BaseCheckbox.Indicator>
|
||||||
|
</BaseCheckbox.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
.textEntry {
|
||||||
|
grid-area: input;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineEntry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
'label label'
|
||||||
|
'input btn';
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redirect {
|
||||||
|
grid-area: btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
@@ -1,65 +1,115 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import { IoArrowForward } from 'react-icons/io5';
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputLeftAddon,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
|
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import Info from '../info/Info';
|
||||||
|
import Input from '../input/input/Input';
|
||||||
|
import AppLink from '../link/app-link/AppLink';
|
||||||
|
import Modal from '../modal/Modal';
|
||||||
|
import Select from '../select/Select';
|
||||||
|
|
||||||
|
import style from './RedirectClientModal.module.scss';
|
||||||
|
|
||||||
interface RedirectClientModalProps {
|
interface RedirectClientModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name: string;
|
||||||
path?: string;
|
currentPath: string;
|
||||||
|
origin: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RedirectClientModal(props: RedirectClientModalProps) {
|
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
|
||||||
const { id, isOpen, name = '', path: currentPath = '', onClose } = props;
|
const { data } = useUrlPresets();
|
||||||
const [path, setPath] = useState(currentPath);
|
const [path, setPath] = useState(currentPath);
|
||||||
|
const [selected, setSelected] = useState('/');
|
||||||
|
|
||||||
const { setRedirect } = setClientRemote;
|
const { setRedirect } = setClientRemote;
|
||||||
|
|
||||||
const handleRedirect = () => {
|
const handleRedirect = (newPath: string) => {
|
||||||
if (path !== currentPath && path !== '') {
|
if (newPath === '/' || newPath === currentPath) {
|
||||||
setRedirect({ target: id, redirect: path });
|
return;
|
||||||
}
|
}
|
||||||
|
setRedirect({ target: id, redirect: newPath });
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const host = window.location.origin;
|
const enabledPresets = data.filter((preset) => preset.enabled);
|
||||||
const canSubmit = path !== currentPath && path !== '';
|
|
||||||
|
const viewOptions = [
|
||||||
|
...navigatorConstants.map((view) => ({
|
||||||
|
value: `/${view.url}`,
|
||||||
|
label: view.label,
|
||||||
|
})),
|
||||||
|
...enabledPresets.map((preset) => ({
|
||||||
|
value: preset.pathAndParams,
|
||||||
|
label: `Preset: ${preset.alias}`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
<Modal
|
||||||
<ModalOverlay />
|
isOpen={isOpen}
|
||||||
<ModalContent>
|
onClose={onClose}
|
||||||
<ModalHeader>Redirect: {name}</ModalHeader>
|
showCloseButton
|
||||||
<ModalCloseButton />
|
showBackdrop
|
||||||
<ModalBody>
|
title={`Redirect: ${name}`}
|
||||||
<InputGroup variant='ontime-filled' size='md'>
|
bodyElements={
|
||||||
<InputLeftAddon>{host}</InputLeftAddon>
|
<>
|
||||||
<Input placeholder='minimal?key=0000ffff' value={path} onChange={(event) => setPath(event.target.value)} />
|
<Info>
|
||||||
</InputGroup>
|
Remotely redirect the client to a different URL. <br />
|
||||||
</ModalBody>
|
Either by selecting a URL Preset or entering a custom path.
|
||||||
<ModalFooter>
|
<br />
|
||||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
<br />
|
||||||
Cancel
|
<AppLink search='settings=feature_settings__urlpresets'>Manage URL Presets</AppLink>
|
||||||
</Button>
|
</Info>
|
||||||
<Button size='md' variant='ontime-filled' onClick={handleRedirect} isDisabled={!canSubmit}>
|
<div>
|
||||||
Submit
|
<span className={style.label}>Select View or URL Preset</span>
|
||||||
</Button>
|
<div className={style.textEntry}>
|
||||||
</ModalFooter>
|
<Select
|
||||||
</ModalContent>
|
fluid
|
||||||
</Modal>
|
options={viewOptions}
|
||||||
|
defaultValue={viewOptions[0].value}
|
||||||
|
onValueChange={(value) => setSelected(value)}
|
||||||
|
disabled={enabledPresets.length === 0}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
aria-label='Redirect to preset'
|
||||||
|
className={style.redirect}
|
||||||
|
disabled={enabledPresets.length === 0 || selected === '/'}
|
||||||
|
onClick={() => handleRedirect(selected)}
|
||||||
|
>
|
||||||
|
Redirect <IoArrowForward />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.inlineEntry}>
|
||||||
|
<span className={style.label}>Enter custom path</span>
|
||||||
|
<label className={style.textEntry}>
|
||||||
|
{origin}
|
||||||
|
<Input
|
||||||
|
placeholder='eg. /minimal?key=0000ffff'
|
||||||
|
fluid
|
||||||
|
value={path}
|
||||||
|
onChange={(event) => setPath(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
aria-label='Redirect'
|
||||||
|
disabled={path === currentPath || path === ''}
|
||||||
|
className={style.redirect}
|
||||||
|
onClick={() => handleRedirect(path)}
|
||||||
|
>
|
||||||
|
Redirect <IoArrowForward />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
|
import Button from '../../../common/components/buttons/Button';
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
|
import Dialog from '../dialog/Dialog';
|
||||||
|
import Input from '../input/input/Input';
|
||||||
|
|
||||||
interface RenameClientModalProps {
|
interface RenameClientModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,8 +12,7 @@ interface RenameClientModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenameClientModal(props: RenameClientModalProps) {
|
export function RenameClientModal({ id, name: currentName = '', isOpen, onClose }: RenameClientModalProps) {
|
||||||
const { id, name: currentName = '', isOpen, onClose } = props;
|
|
||||||
const [name, setName] = useState(currentName);
|
const [name, setName] = useState(currentName);
|
||||||
|
|
||||||
const { setClientName } = setClientRemote;
|
const { setClientName } = setClientRemote;
|
||||||
@@ -36,29 +27,24 @@ export function RenameClientModal(props: RenameClientModalProps) {
|
|||||||
const canSubmit = name !== currentName && name !== '';
|
const canSubmit = name !== currentName && name !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
<Dialog
|
||||||
<ModalOverlay />
|
isOpen={isOpen}
|
||||||
<ModalContent>
|
title={`Rename Client: ${currentName}`}
|
||||||
<ModalHeader>Rename: {currentName}</ModalHeader>
|
showCloseButton
|
||||||
<ModalCloseButton />
|
onClose={onClose}
|
||||||
<ModalBody>
|
bodyElements={
|
||||||
<Input
|
<Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
|
||||||
variant='ontime-filled'
|
}
|
||||||
size='md'
|
footerElements={
|
||||||
placeholder='new name'
|
<>
|
||||||
value={name}
|
<Button variant='subtle' size='large' onClick={onClose}>
|
||||||
onChange={(event) => setName(event.target.value)}
|
|
||||||
/>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}>
|
<Button variant='primary' size='large' onClick={handleRename} disabled={!canSubmit}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</>
|
||||||
</ModalContent>
|
}
|
||||||
</Modal>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
||||||
|
|
||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
|
import { IconType } from 'react-icons';
|
||||||
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
||||||
import { IconType } from '@react-icons/all-files';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { ContextMenuOption } from './ContextMenuOption';
|
import { ContextMenuOption } from './ContextMenuOption';
|
||||||
@@ -23,7 +23,7 @@ export type OptionWithoutGroup = {
|
|||||||
withDivider?: boolean;
|
withDivider?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OptionWithGroup = {
|
type OptionWithGroup = {
|
||||||
label: string;
|
label: string;
|
||||||
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
|
import { IoCopy } from 'react-icons/io5';
|
||||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
||||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
|
||||||
import { IoCopy } from '@react-icons/all-files/io5/IoCopy';
|
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
import { Size } from '../../models/Util.type';
|
import { Size } from '../../models/Util.type';
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
import { IoChevronDown, IoChevronUp } from 'react-icons/io5';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
|
|
||||||
import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp';
|
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
import { millisToDelayString } from '../../utils/dateConfig';
|
import { millisToDelayString } from '../../utils/dateConfig';
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
.dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 10%;
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
z-index: $zindex-dialog;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
padding-inline: 1rem;
|
||||||
|
min-width: min(420px, 90vw);
|
||||||
|
|
||||||
|
background-color: $gray-1250;
|
||||||
|
color: $ui-white;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: $box-shadow-l1;
|
||||||
|
border: 1px solid $gray-1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: $zindex-backdrop;
|
||||||
|
background-color: $backdrop-color;
|
||||||
|
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||||
|
|
||||||
|
&[data-starting-style],
|
||||||
|
&[data-ending-style] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-block: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
max-height: min(80vh, 600px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding-block: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { IoClose } from 'react-icons/io5';
|
||||||
|
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
||||||
|
|
||||||
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
|
import style from './Dialog.module.scss';
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
showBackdrop?: boolean;
|
||||||
|
bodyElements: ReactNode;
|
||||||
|
footerElements: ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Dialog({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
showCloseButton,
|
||||||
|
showBackdrop,
|
||||||
|
bodyElements,
|
||||||
|
footerElements,
|
||||||
|
onClose,
|
||||||
|
}: DialogProps) {
|
||||||
|
return (
|
||||||
|
<BaseDialog.Root
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BaseDialog.Portal>
|
||||||
|
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
||||||
|
<BaseDialog.Popup className={style.dialog}>
|
||||||
|
<div className={style.title}>
|
||||||
|
{title}
|
||||||
|
{showCloseButton && (
|
||||||
|
<IconButton variant='subtle-white' onClick={onClose}>
|
||||||
|
<IoClose />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={style.body}>{bodyElements}</div>
|
||||||
|
<div className={style.footer}>{footerElements}</div>
|
||||||
|
</BaseDialog.Popup>
|
||||||
|
</BaseDialog.Portal>
|
||||||
|
</BaseDialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
+14
@@ -40,3 +40,17 @@
|
|||||||
margin-bottom: $element-inner-spacing;
|
margin-bottom: $element-inner-spacing;
|
||||||
max-width: max-content; // prevent label from taking entire row
|
max-width: max-content; // prevent label from taking entire row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
background-color: $border-color-ondark;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
width: 1px;
|
||||||
|
height: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-3
@@ -1,8 +1,8 @@
|
|||||||
import type { HTMLAttributes, LabelHTMLAttributes } from 'react';
|
import type { HTMLAttributes, LabelHTMLAttributes } from 'react';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IconBaseProps } from 'react-icons';
|
||||||
import { type IconBaseProps } from '@react-icons/all-files/lib';
|
import { IoArrowUp } from 'react-icons/io5';
|
||||||
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './EditorUtils.module.scss';
|
import style from './EditorUtils.module.scss';
|
||||||
|
|
||||||
@@ -27,3 +27,11 @@ export function Label({ children, className, ...elementProps }: LabelHTMLAttribu
|
|||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
|
||||||
|
return <div className={cx([style.separator, style[orientation], className])} {...elementProps} />;
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ import React from 'react';
|
|||||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
|
||||||
import { runtimeStore } from '@/common/stores/runtime';
|
import { hasConnected, reconnectAttempts } from '../../../common/utils/socket';
|
||||||
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
import { runtimeStore } from '../../stores/runtime';
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
|
|
||||||
@@ -26,17 +26,22 @@ class ErrorBoundary extends React.Component {
|
|||||||
error,
|
error,
|
||||||
errorInfo: info,
|
errorInfo: info,
|
||||||
});
|
});
|
||||||
|
this.reportContent = `${error} ${info.componentStack}`;
|
||||||
|
|
||||||
|
const appState = runtimeStore.getState();
|
||||||
|
if (appState.ping < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Sentry.withScope((scope) => {
|
Sentry.withScope((scope) => {
|
||||||
scope.setExtras({
|
scope.setExtras({
|
||||||
error,
|
error,
|
||||||
store: runtimeStore.getState(),
|
store: appState,
|
||||||
hasSocket: { hasConnected, shouldReconnect, reconnectAttempts },
|
hasSocket: { hasConnected, reconnectAttempts },
|
||||||
});
|
});
|
||||||
const eventId = Sentry.captureException(error);
|
const eventId = Sentry.captureException(error);
|
||||||
this.setState({ eventId, info });
|
this.setState({ eventId, info });
|
||||||
});
|
});
|
||||||
this.reportContent = `${error} ${info.componentStack}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IconBaseProps } from 'react-icons';
|
||||||
|
import { IoLink } from 'react-icons/io5';
|
||||||
|
|
||||||
|
export default function RotatedLink(linkProps: IconBaseProps) {
|
||||||
|
return <IoLink style={{ transform: 'rotate(-45deg)' }} {...linkProps} />;
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 100;
|
z-index: $zindex-modal;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.infoLabel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $element-spacing;
|
||||||
|
|
||||||
|
background-color: $gray-1200;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: $text-body-size;
|
||||||
|
padding: 1rem;
|
||||||
|
color: $gray-200;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
min-width: 1.5rem;
|
||||||
|
align-self: start;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
svg {
|
||||||
|
color: $info-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
svg {
|
||||||
|
color: $orange-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
svg {
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { IoAlertCircle, IoWarning } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './Info.module.scss';
|
||||||
|
|
||||||
|
interface InfoProps {
|
||||||
|
className?: string;
|
||||||
|
type?: 'info' | 'warning' | 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Info({ className, type = 'info', children }: PropsWithChildren<InfoProps>) {
|
||||||
|
return (
|
||||||
|
<div className={cx([style.infoLabel, style[type], className])}>
|
||||||
|
{type === 'info' && <IoAlertCircle />}
|
||||||
|
{type === 'warning' && <IoWarning />}
|
||||||
|
{type === 'error' && <IoWarning />}
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
import { IoBan } from 'react-icons/io5';
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useController, UseControllerProps } from 'react-hook-form';
|
import { useController, UseControllerProps } from 'react-hook-form';
|
||||||
import { IoEyedrop } from '@react-icons/all-files/io5/IoEyedrop';
|
import { IoEyedrop } from 'react-icons/io5';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import PopoverPicker from '../../../../common/components/input/popover-picker/PopoverPicker';
|
import { debounce } from '../../../utils/debounce';
|
||||||
import { debounce } from '../../../../common/utils/debounce';
|
|
||||||
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
||||||
|
import PopoverPicker from '../popover-picker/PopoverPicker';
|
||||||
|
|
||||||
import style from './SwatchSelect.module.scss';
|
import style from './SwatchSelect.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ $input-font-size: 15px;
|
|||||||
|
|
||||||
.inputField {
|
.inputField {
|
||||||
font-size: $input-font-size;
|
font-size: $input-font-size;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.5px;
|
||||||
max-width: 7em;
|
max-width: 7em;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
color: $ontime-delay-text
|
color: $ontime-delay-text
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
|||||||
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
|
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
|
||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../../hooks/useEventAction';
|
import { useEntryActions } from '../../../hooks/useEntryAction';
|
||||||
|
|
||||||
import style from './DelayInput.module.scss';
|
import style from './DelayInput.module.scss';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ interface DelayInputProps {
|
|||||||
|
|
||||||
export default function DelayInput(props: DelayInputProps) {
|
export default function DelayInput(props: DelayInputProps) {
|
||||||
const { eventId, duration } = props;
|
const { eventId, duration } = props;
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEntry } = useEntryActions();
|
||||||
|
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
@@ -54,7 +54,7 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitChange = (value: number) => {
|
const submitChange = (value: number) => {
|
||||||
updateEvent({
|
updateEntry({
|
||||||
id: eventId,
|
id: eventId,
|
||||||
duration: value,
|
duration: value,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
.input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $gray-200;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
padding-inline: 0.5em;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: $gray-1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:read-only) {
|
||||||
|
background-color: $gray-1000;
|
||||||
|
border: 1px solid $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $gray-500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle {
|
||||||
|
background-color: $gray-1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './Input.module.scss';
|
||||||
|
|
||||||
|
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
variant?: 'subtle' | 'ghosted';
|
||||||
|
height?: 'medium' | 'large';
|
||||||
|
fluid?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||||
|
{ className, variant = 'subtle', height = 'medium', fluid, ...inputProps },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type='text'
|
||||||
|
autoCorrect='off'
|
||||||
|
autoComplete='off'
|
||||||
|
spellCheck='false'
|
||||||
|
className={cx([style.input, style[variant], style[height], fluid && style.fluid, className])}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Input;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks';
|
import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks';
|
||||||
|
|
||||||
interface UseReactiveTextInputReturn {
|
interface UseReactiveTextInputReturn {
|
||||||
@@ -17,9 +17,12 @@ export default function useReactiveTextInput(
|
|||||||
submitOnCtrlEnter?: boolean;
|
submitOnCtrlEnter?: boolean;
|
||||||
onCancelUpdate?: () => void;
|
onCancelUpdate?: () => void;
|
||||||
allowSubmitSameValue?: boolean;
|
allowSubmitSameValue?: boolean;
|
||||||
|
allowKeyboardNavigation?: boolean;
|
||||||
},
|
},
|
||||||
): UseReactiveTextInputReturn {
|
): UseReactiveTextInputReturn {
|
||||||
const [text, setText] = useState<string>(initialText);
|
const [text, setText] = useState<string>(initialText);
|
||||||
|
// track whether we are submitting via a submit key (eg enter) and avoid submitting again on blur
|
||||||
|
const isKeyboardSubmitting = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof initialText === 'undefined') {
|
if (typeof initialText === 'undefined') {
|
||||||
@@ -58,7 +61,14 @@ export default function useReactiveTextInput(
|
|||||||
setText(cleanVal);
|
setText(cleanVal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (options?.allowKeyboardNavigation) {
|
||||||
|
ref.current?.parentElement?.focus(); // Focus on parent element to continue keyboard navigation
|
||||||
|
} else {
|
||||||
|
ref.current?.blur();
|
||||||
|
}
|
||||||
|
}); // Immediate timeout to ensure text is set before blurring
|
||||||
},
|
},
|
||||||
[initialText, options, ref, submitCallback],
|
[initialText, options, ref, submitCallback],
|
||||||
);
|
);
|
||||||
@@ -70,27 +80,73 @@ export default function useReactiveTextInput(
|
|||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
// No need to update if it hasn't changed
|
// No need to update if it hasn't changed
|
||||||
setText(initialText);
|
setText(initialText);
|
||||||
|
// force the text to be the initial value
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.value = initialText;
|
||||||
|
}
|
||||||
options?.onCancelUpdate?.();
|
options?.onCancelUpdate?.();
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before blurring
|
||||||
}, [initialText, options, ref]);
|
}, [initialText, options, ref]);
|
||||||
|
|
||||||
const keyHandler = useMemo(() => {
|
const keyHandler = useMemo(() => {
|
||||||
const hotKeys: HotkeyItem[] = [['Escape', handleEscape, { preventDefault: true }]];
|
const hotKeys: HotkeyItem[] = [
|
||||||
|
[
|
||||||
|
'Escape',
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
handleEscape();
|
||||||
|
},
|
||||||
|
{ preventDefault: true },
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
if (options?.submitOnEnter) {
|
if (options?.submitOnEnter) {
|
||||||
hotKeys.push(['Enter', () => handleSubmit(text)]);
|
hotKeys.push(['Enter', () => {
|
||||||
|
isKeyboardSubmitting.current = true;
|
||||||
|
handleSubmit(text);
|
||||||
|
// clear flag after blur has been processed
|
||||||
|
setTimeout(() => {
|
||||||
|
isKeyboardSubmitting.current = false;
|
||||||
|
}, 0);
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.submitOnCtrlEnter) {
|
if (options?.submitOnCtrlEnter) {
|
||||||
hotKeys.push(['mod + Enter', () => handleSubmit(text)]);
|
hotKeys.push(['mod + Enter', () => {
|
||||||
|
isKeyboardSubmitting.current = true;
|
||||||
|
handleSubmit(text);
|
||||||
|
// clear flag after blur has been processed
|
||||||
|
setTimeout(() => {
|
||||||
|
isKeyboardSubmitting.current = false;
|
||||||
|
}, 0);
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
return getHotkeyHandler(hotKeys);
|
|
||||||
|
const hotKeyHandler = getHotkeyHandler(hotKeys);
|
||||||
|
|
||||||
|
return (event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
// allow moving in input field with arrow keys
|
||||||
|
if (
|
||||||
|
event.key === 'ArrowLeft' ||
|
||||||
|
event.key === 'ArrowRight' ||
|
||||||
|
event.key === 'ArrowUp' ||
|
||||||
|
event.key === 'ArrowDown'
|
||||||
|
) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
hotKeyHandler(event);
|
||||||
|
};
|
||||||
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, text]);
|
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, text]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: text,
|
value: text,
|
||||||
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
||||||
onBlur: (event: ChangeEvent) => handleSubmit((event.target as HTMLInputElement).value),
|
onBlur: (event: ChangeEvent) => {
|
||||||
|
if (!isKeyboardSubmitting.current) {
|
||||||
|
handleSubmit((event.target as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
},
|
||||||
onKeyDown: keyHandler,
|
onKeyDown: keyHandler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
@use '../../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $gray-200;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
padding-inline: 0.5em;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: $gray-1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:read-only) {
|
||||||
|
background-color: $gray-1000;
|
||||||
|
border: 1px solid $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $gray-500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle {
|
||||||
|
background-color: $gray-1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { forwardRef, TextareaHTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './Textarea.module.scss';
|
||||||
|
|
||||||
|
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||||
|
variant?: 'subtle' | 'ghosted';
|
||||||
|
fluid?: boolean;
|
||||||
|
resize?: 'none' | 'both' | 'horizontal' | 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function TextArea(
|
||||||
|
{ className, variant = 'subtle', fluid, rows = 5, resize = 'none', style: customStyle, ...textareaProps },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
autoCorrect='off'
|
||||||
|
autoComplete='off'
|
||||||
|
spellCheck='false'
|
||||||
|
rows={rows}
|
||||||
|
style={{ ...customStyle, resize }}
|
||||||
|
className={cx([style.textarea, style[variant], fluid && style.fluid, className])}
|
||||||
|
{...textareaProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Textarea;
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
import Input from '../input/Input';
|
||||||
|
|
||||||
|
import style from './TimeInput.module.scss';
|
||||||
|
|
||||||
|
interface NullableTimeInputProps<T extends string> {
|
||||||
|
id?: T;
|
||||||
|
name: T;
|
||||||
|
submitHandler: (field: T, value: string) => void;
|
||||||
|
time?: number | null;
|
||||||
|
emptyDisplay: string;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
align?: 'left' | 'center';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NullableTimeInput<T extends string>(props: NullableTimeInputProps<T>) {
|
||||||
|
const { id, name, submitHandler, time, emptyDisplay, placeholder, disabled, align = 'center', className } = props;
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
const ignoreChange = useRef(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Resets input value to given
|
||||||
|
*/
|
||||||
|
const resetValue = useCallback(() => {
|
||||||
|
if (typeof time !== 'number' || isNaN(time)) {
|
||||||
|
setValue(emptyDisplay);
|
||||||
|
} else {
|
||||||
|
setValue(millisToString(time));
|
||||||
|
}
|
||||||
|
}, [emptyDisplay, time]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Selects input text on focus
|
||||||
|
*/
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
inputRef.current?.select();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Submit handler
|
||||||
|
* @param {string} newValue
|
||||||
|
*/
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
// skip if user deleted and time is already null
|
||||||
|
if (newValue === '' && time === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip if the value evaluates to the same thing
|
||||||
|
const valueInMillis = parseUserTime(newValue);
|
||||||
|
if (valueInMillis === time) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitHandler(name, newValue);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[name, submitHandler, time],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Prepare time fields
|
||||||
|
* @param {string} value string to be parsed
|
||||||
|
*/
|
||||||
|
const validateAndSubmit = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
const success = handleSubmit(newValue);
|
||||||
|
if (!success) {
|
||||||
|
resetValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSubmit, resetValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Handles common keys for submit and cancel
|
||||||
|
* @param {KeyboardEvent} event
|
||||||
|
*/
|
||||||
|
const onKeyDownHandler = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
ignoreChange.current = true;
|
||||||
|
inputRef.current?.blur();
|
||||||
|
resetValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[resetValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBlurHandler = useCallback(
|
||||||
|
(event: FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (ignoreChange.current) {
|
||||||
|
ignoreChange.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||||
|
},
|
||||||
|
[validateAndSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resetValue();
|
||||||
|
}, [resetValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={inputRef}
|
||||||
|
data-testid={`time-input-${name}`}
|
||||||
|
className={cx([style.timeInput, className])}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onChange={(event) => setValue(event.target.value)}
|
||||||
|
onBlur={onBlurHandler}
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
|
value={value}
|
||||||
|
maxLength={8}
|
||||||
|
autoComplete='off'
|
||||||
|
style={{
|
||||||
|
textAlign: align,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
.timeInput {
|
.timeInput {
|
||||||
letter-spacing: 1px;
|
width: 100%;
|
||||||
width: 6.5em;
|
max-width: 7.5em;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Input } from '@chakra-ui/react';
|
|
||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEmitLog } from '../../../stores/logger';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
import Input from '../input/Input';
|
||||||
|
|
||||||
|
import style from './TimeInput.module.scss';
|
||||||
|
|
||||||
interface TimeInputProps<T extends string> {
|
interface TimeInputProps<T extends string> {
|
||||||
id?: T;
|
id?: T;
|
||||||
name: T;
|
name: T;
|
||||||
submitHandler: (field: T, value: string) => void;
|
submitHandler: (field: T, value: string) => void;
|
||||||
time?: number;
|
time?: number;
|
||||||
placeholder: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
align?: 'left' | 'center';
|
align?: 'left' | 'center';
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
||||||
const { id, name, submitHandler, time = 0, placeholder, disabled, align = 'center', className } = props;
|
const { id, name, submitHandler, time, placeholder, disabled, align = 'center', className } = props;
|
||||||
const { emitError } = useEmitLog();
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
const ignoreChange = useRef(false);
|
const ignoreChange = useRef(false);
|
||||||
@@ -26,13 +27,12 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
|||||||
* @description Resets input value to given
|
* @description Resets input value to given
|
||||||
*/
|
*/
|
||||||
const resetValue = useCallback(() => {
|
const resetValue = useCallback(() => {
|
||||||
try {
|
if (typeof time !== 'number' || isNaN(time)) {
|
||||||
|
setValue('00:00:00');
|
||||||
|
} else {
|
||||||
setValue(millisToString(time));
|
setValue(millisToString(time));
|
||||||
} catch (error) {
|
|
||||||
setValue(millisToString(0));
|
|
||||||
emitError(`Unable to parse time ${time}: ${error}`);
|
|
||||||
}
|
}
|
||||||
}, [emitError, time]);
|
}, [time]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Selects input text on focus
|
* @description Selects input text on focus
|
||||||
@@ -113,32 +113,27 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (time == null) return;
|
|
||||||
resetValue();
|
resetValue();
|
||||||
}, [resetValue, time]);
|
}, [resetValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size='sm'
|
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
data-testid={`time-input-${name}`}
|
data-testid={`time-input-${name}`}
|
||||||
className={className}
|
className={cx([style.timeInput, className])}
|
||||||
fontSize='1rem'
|
|
||||||
type='text'
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
variant='ontime-filled'
|
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onBlur={onBlurHandler}
|
onBlur={onBlurHandler}
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
value={value}
|
value={value}
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
maxWidth='7.5em'
|
|
||||||
letterSpacing='1px'
|
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
textAlign={align}
|
style={{
|
||||||
|
textAlign: align,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.link {
|
||||||
|
color: $label-gray;
|
||||||
|
font-size: $text-body-size;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
color: $blue-400;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { type PropsWithChildren } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import style from './AppLink.module.scss';
|
||||||
|
|
||||||
|
interface AppLinkProps {
|
||||||
|
search: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to navigate to an editor link inside the same window
|
||||||
|
* Handles the path to respect Ontime Clouds base URL
|
||||||
|
*/
|
||||||
|
export default function AppLink(props: PropsWithChildren<AppLinkProps>) {
|
||||||
|
const { search, children } = props;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleClick = () => navigate({ search });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleClick} className={style.link}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
-6
@@ -1,8 +1,8 @@
|
|||||||
import { MouseEvent, ReactNode } from 'react';
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
|
import { IoOpenOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
import { openLink } from '../../utils/linkUtils';
|
import { openLink } from '../../../utils/linkUtils';
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './ExternalLink.module.scss';
|
import style from './ExternalLink.module.scss';
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ interface ExternalLinkProps {
|
|||||||
|
|
||||||
export default function ExternalLink(props: ExternalLinkProps) {
|
export default function ExternalLink(props: ExternalLinkProps) {
|
||||||
const { href, inline, children } = props;
|
const { href, inline, children } = props;
|
||||||
const classes = cx([style.link, inline ? style.inline : null]);
|
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -22,8 +21,14 @@ export default function ExternalLink(props: ExternalLinkProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href='#!' target='_blank' rel='noreferrer' className={classes} onClick={handleClick}>
|
<a
|
||||||
{children} <IoOpenOutline />
|
href='#!'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
className={cx([style.link, inline && style.inline])}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{children} <IoOpenOutline style={{ fontSize: '1em' }} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
$loader-size: 4rem;
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
background-color: $black-10;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
width: $loader-size;
|
|
||||||
height: $loader-size;
|
|
||||||
background: $blue-500;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: animloader 1s ease-in infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animloader {
|
|
||||||
0% {
|
|
||||||
transform: scale(0);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animloader {
|
|
||||||
0% {
|
|
||||||
transform: scale(0);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import style from './LoaderOverlay.module.scss';
|
|
||||||
|
|
||||||
export default function LoaderOverlay() {
|
|
||||||
return (
|
|
||||||
<div className={style.overlay}>
|
|
||||||
<span className={style.loader} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 10vh;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
padding-inline: 1rem;
|
||||||
|
min-width: min(680px, 90vw);
|
||||||
|
min-height: min(200px, 10vh);
|
||||||
|
max-width: min(680px, 90vw);
|
||||||
|
|
||||||
|
background-color: $gray-1250;
|
||||||
|
color: $ui-white;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: $box-shadow-l1;
|
||||||
|
border: 1px solid $gray-1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: $backdrop-color;
|
||||||
|
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||||
|
|
||||||
|
&[data-starting-style],
|
||||||
|
&[data-ending-style] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-block: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding-block: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { IoClose } from 'react-icons/io5';
|
||||||
|
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
||||||
|
|
||||||
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
|
import style from './Modal.module.scss';
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
showBackdrop?: boolean;
|
||||||
|
bodyElements: ReactNode;
|
||||||
|
footerElements?: ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Modal({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
showCloseButton,
|
||||||
|
showBackdrop,
|
||||||
|
bodyElements,
|
||||||
|
footerElements,
|
||||||
|
onClose,
|
||||||
|
}: ModalProps) {
|
||||||
|
return (
|
||||||
|
<BaseDialog.Root
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) onClose();
|
||||||
|
}}
|
||||||
|
dismissible={false}
|
||||||
|
>
|
||||||
|
<BaseDialog.Portal>
|
||||||
|
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
||||||
|
<BaseDialog.Popup className={style.modal}>
|
||||||
|
<div className={style.title}>
|
||||||
|
{title}
|
||||||
|
{showCloseButton && (
|
||||||
|
<IconButton variant='subtle-white' onClick={onClose}>
|
||||||
|
<IoClose />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={style.body}>{bodyElements}</div>
|
||||||
|
<div className={style.footer}>{footerElements}</div>
|
||||||
|
</BaseDialog.Popup>
|
||||||
|
</BaseDialog.Portal>
|
||||||
|
</BaseDialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
$progress-bar-size: 12px;
|
$progress-bar-size: 1.5rem;
|
||||||
$progress-bar-br: 3px;
|
$progress-bar-br: 3px;
|
||||||
|
|
||||||
.multiprogress-bar {
|
.multiprogress-bar {
|
||||||
@@ -53,3 +53,6 @@ $progress-bar-br: 3px;
|
|||||||
.multiprogress-bar__bg-danger {
|
.multiprogress-bar__bg-danger {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.multiprogress-bar__bg-danger--overtime {
|
||||||
|
background: var(--timer-overtime-color-override, $timer-finished-color);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
|
||||||
import { getProgress } from '../../utils/getProgress';
|
import { getProgress } from '../../utils/getProgress';
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import './MultiPartProgressBar.scss';
|
import './MultiPartProgressBar.scss';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ interface MultiPartProgressBar {
|
|||||||
danger?: MaybeNumber;
|
danger?: MaybeNumber;
|
||||||
dangerColor: string;
|
dangerColor: string;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
hideOvertime?: boolean;
|
||||||
ignoreCssOverride?: boolean;
|
ignoreCssOverride?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -27,6 +29,7 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
|||||||
danger,
|
danger,
|
||||||
dangerColor,
|
dangerColor,
|
||||||
hidden,
|
hidden,
|
||||||
|
hideOvertime,
|
||||||
ignoreCssOverride,
|
ignoreCssOverride,
|
||||||
className = '',
|
className = '',
|
||||||
} = props;
|
} = props;
|
||||||
@@ -34,12 +37,17 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
|||||||
const percentRemaining = 100 - getProgress(now, complete);
|
const percentRemaining = 100 - getProgress(now, complete);
|
||||||
const dangerWidth = danger ? 100 - getProgress(danger, complete) : 0;
|
const dangerWidth = danger ? 100 - getProgress(danger, complete) : 0;
|
||||||
const warningWidth = warning ? 100 - dangerWidth - getProgress(warning, complete) : 0;
|
const warningWidth = warning ? 100 - dangerWidth - getProgress(warning, complete) : 0;
|
||||||
|
const isOvertime = now !== null && now < 0;
|
||||||
|
const showOvertime = isOvertime && !hideOvertime;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${
|
className={cx([
|
||||||
ignoreCssOverride ? 'multiprogress-bar--ignore-css-override' : ''
|
'multiprogress-bar',
|
||||||
} ${className}`}
|
hidden && 'multiprogress-bar--hidden',
|
||||||
|
ignoreCssOverride && 'multiprogress-bar--ignore-css-override',
|
||||||
|
className,
|
||||||
|
])}
|
||||||
>
|
>
|
||||||
{now !== null && (
|
{now !== null && (
|
||||||
<>
|
<>
|
||||||
@@ -50,8 +58,8 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
|||||||
style={{ width: `${warningWidth}%`, backgroundColor: warningColor }}
|
style={{ width: `${warningWidth}%`, backgroundColor: warningColor }}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className='multiprogress-bar__bg-danger'
|
className={cx(['multiprogress-bar__bg-danger', showOvertime && 'multiprogress-bar__bg-danger--overtime'])}
|
||||||
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
|
style={{ width: `${dangerWidth}%`, backgroundColor: showOvertime ? undefined : dangerColor }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='multiprogress-bar__indicator'>
|
<div className='multiprogress-bar__indicator'>
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
|
||||||
|
|
||||||
import { useFadeOutOnInactivity } from '../../../common/hooks/useFadeOutOnInactivity';
|
|
||||||
|
|
||||||
import style from './NavigationMenu.module.scss';
|
|
||||||
|
|
||||||
interface FloatingNavigationProps {
|
|
||||||
toggleMenu: () => void;
|
|
||||||
toggleSettings: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FloatingNavigation(props: FloatingNavigationProps) {
|
|
||||||
const { toggleMenu, toggleSettings } = props;
|
|
||||||
const isButtonShown = useFadeOutOnInactivity();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${style.buttonContainer} ${!isButtonShown ? style.hidden : ''}`}>
|
|
||||||
<button
|
|
||||||
onClick={toggleMenu}
|
|
||||||
aria-label='toggle menu'
|
|
||||||
className={style.navButton}
|
|
||||||
data-testid='navigation__toggle-menu'
|
|
||||||
>
|
|
||||||
<IoApps />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={style.button}
|
|
||||||
onClick={toggleSettings}
|
|
||||||
aria-label='toggle settings'
|
|
||||||
data-testid='navigation__toggle-settings'
|
|
||||||
>
|
|
||||||
<IoSettingsOutline />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,114 +1,68 @@
|
|||||||
@use '../../../theme/mixins' as *;
|
|
||||||
|
|
||||||
$menu-hover-bg: $gray-1350;
|
|
||||||
$menu-focus-bg: $gray-1300;
|
|
||||||
|
|
||||||
$icon-color: $ui-white;
|
|
||||||
$button-bg: $gray-1050;
|
|
||||||
$button-size: 3rem;
|
|
||||||
|
|
||||||
.buttonContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
row-gap: 1rem;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
transition-property: opacity;
|
|
||||||
transition-duration: 0.3s;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 12;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: $icon-color;
|
|
||||||
background-color: $button-bg;
|
|
||||||
width: $button-size;
|
|
||||||
height: $button-size;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lockIcon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: transparent;
|
|
||||||
color: $active-indicator;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navButton {
|
|
||||||
@extend .button;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
@include action-link;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $menu-hover-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: $border-color-ondark;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
background-color: $menu-focus-bg;
|
|
||||||
border-left: 2px solid $action-text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
background-color: $menu-hover-bg;
|
|
||||||
border-left: 4px solid $action-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkIcon {
|
|
||||||
margin-left: auto;
|
|
||||||
@include rotate-fourty-five;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
border-color: $border-color-ondark;
|
border-color: $border-color-ondark;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.backdrop {
|
||||||
font-size: calc(1rem - 2px);
|
position: fixed;
|
||||||
margin-left: 1rem;
|
inset: 0;
|
||||||
color: $gray-700;
|
z-index: $zindex-backdrop;
|
||||||
|
background-color: $backdrop-color;
|
||||||
|
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||||
|
|
||||||
|
&[data-starting-style],
|
||||||
|
&[data-ending-style] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
.drawer {
|
||||||
margin-top: auto;
|
box-sizing: border-box;
|
||||||
margin-bottom: 1rem;
|
position: fixed;
|
||||||
}
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: $zindex-dialog;
|
||||||
|
|
||||||
|
width: 22rem;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
.interfaces {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
padding-block: 1rem;
|
||||||
|
|
||||||
|
background-color: $gray-1250;
|
||||||
|
color: $ui-white;
|
||||||
|
border-right: 1px solid $gray-1100;
|
||||||
|
|
||||||
|
&[data-open] {
|
||||||
|
transform: translateX(0%);
|
||||||
|
transition: transform 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-starting-style],
|
||||||
|
&[data-ending-style] {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 500ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||||
|
}
|
||||||
|
|
||||||
|
// take the whole screen in mobile devices
|
||||||
|
@media (max-width: $min-tablet) {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.goIcon {
|
.header {
|
||||||
@include rotate-fourty-five;
|
display: flex;
|
||||||
margin-left: 0.25rem;
|
align-items: center;
|
||||||
margin-bottom: 0.25rem;
|
justify-content: space-between;
|
||||||
|
height: 3.5rem;
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,20 @@
|
|||||||
import { memo, PropsWithChildren, useRef } from 'react';
|
import { memo } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import {
|
import { Dialog } from '@base-ui-components/react/dialog';
|
||||||
Drawer,
|
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||||
DrawerBody,
|
|
||||||
DrawerCloseButton,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerOverlay,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useFullscreen } from '@mantine/hooks';
|
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|
||||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
|
||||||
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
|
|
||||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
|
||||||
|
|
||||||
import { isLocalhost } from '../../../externals';
|
import { isLocalhost } from '../../../externals';
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import useClickOutside from '../../hooks/useClickOutside';
|
|
||||||
import { useElectronEvent } from '../../hooks/useElectronEvent';
|
|
||||||
import useInfo from '../../hooks-query/useInfo';
|
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
import { isKeyEnter } from '../../utils/keyEvent';
|
import IconButton from '../buttons/IconButton';
|
||||||
import { handleLinks, linkToOtherHost, openLink } from '../../utils/linkUtils';
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
|
||||||
import { RenameClientModal } from '../client-modal/RenameClientModal';
|
import { RenameClientModal } from '../client-modal/RenameClientModal';
|
||||||
import CopyTag from '../copy-tag/CopyTag';
|
|
||||||
|
import ClientLink from './client-link/ClientLink';
|
||||||
|
import EditorNavigation from './editor-navigation/EditorNavigation';
|
||||||
|
import NavigationMenuItem from './navigation-menu-item/NavigationMenuItem';
|
||||||
|
import OtherAddresses from './other-addresses/OtherAddresses';
|
||||||
|
|
||||||
import style from './NavigationMenu.module.scss';
|
import style from './NavigationMenu.module.scss';
|
||||||
|
|
||||||
@@ -37,78 +23,49 @@ interface NavigationMenuProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenu(props: NavigationMenuProps) {
|
export default memo(NavigationMenu);
|
||||||
const { isOpen, onClose } = props;
|
function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||||
|
|
||||||
const id = useClientStore((store) => store.id);
|
const id = useClientStore((store) => store.id);
|
||||||
const name = useClientStore((store) => store.name);
|
const name = useClientStore((store) => store.name);
|
||||||
|
|
||||||
const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure();
|
const [isRenameOpen, handlers] = useDisclosure(false);
|
||||||
const { fullscreen, toggle } = useFullscreen();
|
const { fullscreen, toggle } = useFullscreen();
|
||||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
return (
|
||||||
|
<Dialog.Root
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Backdrop className={style.backdrop} />
|
||||||
|
<RenameClientModal id={id} name={name} isOpen={isRenameOpen} onClose={handlers.close} />
|
||||||
|
<Dialog.Popup className={style.drawer}>
|
||||||
|
<div className={style.header}>
|
||||||
|
<Dialog.Title>Ontime</Dialog.Title>
|
||||||
|
<IconButton variant='subtle-white' size='large' onClick={onClose}>
|
||||||
|
<IoClose />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className={style.body}>
|
||||||
|
<NavigationMenuItem active={fullscreen} onClick={toggle}>
|
||||||
|
Toggle Fullscreen
|
||||||
|
{fullscreen ? <IoContract /> : <IoExpand />}
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem active={mirror} onClick={toggleMirror}>
|
||||||
|
Flip Screen
|
||||||
|
<IoSwapVertical />
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem onClick={handlers.open}>Rename Client</NavigationMenuItem>
|
||||||
|
|
||||||
useClickOutside(menuRef, () => onClose);
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<div id='navigation-menu-portal' ref={menuRef}>
|
|
||||||
<RenameClientModal id={id} name={name} isOpen={isOpenRename} onClose={onCloseRename} />
|
|
||||||
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerContent maxWidth='22rem'>
|
|
||||||
<DrawerHeader>
|
|
||||||
<DrawerCloseButton size='lg' />
|
|
||||||
Ontime
|
|
||||||
</DrawerHeader>
|
|
||||||
<DrawerBody padding={0}>
|
|
||||||
<div className={style.buttonsContainer}>
|
|
||||||
<div
|
|
||||||
className={cx([style.link, fullscreen && style.current])}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={toggle}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && toggle();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Toggle Fullscreen
|
|
||||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cx([style.link, mirror && style.current])}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={() => toggleMirror()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && toggleMirror();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Flip Screen
|
|
||||||
<IoSwapVertical />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={style.link}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={onRenameOpen}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && onRenameOpen();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Rename Client
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className={style.separator} />
|
<hr className={style.separator} />
|
||||||
<Link
|
|
||||||
to='/editor'
|
<EditorNavigation />
|
||||||
tabIndex={0}
|
|
||||||
className={`${style.link} ${location.pathname === '/editor' && style.current}`}
|
|
||||||
>
|
|
||||||
<IoLockClosedOutline />
|
|
||||||
Editor
|
|
||||||
</Link>
|
|
||||||
<ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}>
|
<ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}>
|
||||||
<IoLockClosedOutline />
|
<IoLockClosedOutline />
|
||||||
Cuesheet
|
Cuesheet
|
||||||
@@ -117,86 +74,23 @@ function NavigationMenu(props: NavigationMenuProps) {
|
|||||||
<IoLockClosedOutline />
|
<IoLockClosedOutline />
|
||||||
Operator
|
Operator
|
||||||
</ClientLink>
|
</ClientLink>
|
||||||
|
|
||||||
<hr className={style.separator} />
|
<hr className={style.separator} />
|
||||||
|
|
||||||
{navigatorConstants.map((route) => (
|
{navigatorConstants.map((route) => (
|
||||||
<ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}>
|
<ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}>
|
||||||
{route.label}
|
{route.label}
|
||||||
</ClientLink>
|
</ClientLink>
|
||||||
))}
|
))}
|
||||||
{isLocalhost && <OtherAddresses currentLocation={location.pathname} />}
|
</div>
|
||||||
</DrawerBody>
|
|
||||||
</DrawerContent>
|
{isLocalhost && (
|
||||||
</Drawer>
|
<div>
|
||||||
</div>,
|
<OtherAddresses currentLocation={location.pathname} />
|
||||||
document.body,
|
</div>
|
||||||
|
)}
|
||||||
|
</Dialog.Popup>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OtherAddressesProps {
|
|
||||||
currentLocation: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function OtherAddresses(props: OtherAddressesProps) {
|
|
||||||
const { currentLocation } = props;
|
|
||||||
const { data } = useInfo();
|
|
||||||
|
|
||||||
// there is no point showing this if we only have one interface
|
|
||||||
if (data.networkInterfaces.length < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.bottom}>
|
|
||||||
<div className={style.sectionHeader}>Accessible on external networks</div>
|
|
||||||
<div className={style.interfaces}>
|
|
||||||
{data?.networkInterfaces?.map((nif) => {
|
|
||||||
if (nif.name === 'localhost') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const address = linkToOtherHost(nif.address, currentLocation);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CopyTag
|
|
||||||
key={nif.name}
|
|
||||||
copyValue={address}
|
|
||||||
onClick={() => openLink(address)}
|
|
||||||
label='Copy IP or navigate to address'
|
|
||||||
>
|
|
||||||
{nif.address} <IoArrowUp className={style.goIcon} />
|
|
||||||
</CopyTag>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClientLinkProps {
|
|
||||||
current: boolean;
|
|
||||||
to: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClientLink(props: PropsWithChildren<ClientLinkProps>) {
|
|
||||||
const { current, to, children } = props;
|
|
||||||
const { isElectron } = useElectronEvent();
|
|
||||||
|
|
||||||
const classes = cx([style.link, current && style.current]);
|
|
||||||
|
|
||||||
if (isElectron) {
|
|
||||||
return (
|
|
||||||
<button className={classes} tabIndex={0} onClick={(event) => handleLinks(event, to)}>
|
|
||||||
{children}
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={`/${to}`} className={classes} tabIndex={0}>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(NavigationMenu);
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import NavigationMenu from './NavigationMenu';
|
|
||||||
|
|
||||||
interface ProductionNavigationMenuProps {
|
|
||||||
isMenuOpen: boolean;
|
|
||||||
onMenuClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProductionNavigationMenu(props: ProductionNavigationMenuProps) {
|
|
||||||
const { isMenuOpen, onMenuClose } = props;
|
|
||||||
|
|
||||||
return <NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(ProductionNavigationMenu);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
|
|
||||||
|
|
||||||
import { useFadeOutOnInactivity } from '../../../common/hooks/useFadeOutOnInactivity';
|
|
||||||
|
|
||||||
import style from './NavigationMenu.module.scss';
|
|
||||||
|
|
||||||
export default function ViewLockedIcon() {
|
|
||||||
const isLockIconShown = useFadeOutOnInactivity();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${style.buttonContainer} ${!isLockIconShown ? style.hidden : ''}`}>
|
|
||||||
<IoLockClosedOutline className={style.lockIcon} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,39 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
import FloatingNavigation from './floating-navigation/FloatingNavigation';
|
||||||
|
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
|
||||||
import FloatingNavigation from './FloatingNavigation';
|
|
||||||
import NavigationMenu from './NavigationMenu';
|
import NavigationMenu from './NavigationMenu';
|
||||||
import ViewLockedIcon from './ViewLockedIcon';
|
import useViewEditor from './useViewEditor';
|
||||||
|
|
||||||
function ViewNavigationMenu() {
|
interface ViewNavigationMenuProps {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
isLockable?: boolean;
|
||||||
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
|
supressSettings?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const isViewLocked = isStringBoolean(searchParams.get('locked'));
|
export default memo(ViewNavigationMenu);
|
||||||
|
function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuProps) {
|
||||||
|
const [isMenuOpen, menuHandler] = useDisclosure();
|
||||||
|
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable });
|
||||||
|
|
||||||
const showEditFormDrawer = useCallback(() => {
|
useHotkeys([
|
||||||
searchParams.set('edit', 'true');
|
[
|
||||||
setSearchParams(searchParams);
|
'Space',
|
||||||
}, [searchParams, setSearchParams]);
|
() => {
|
||||||
|
if (isViewLocked) return;
|
||||||
const toggleMenu = () => (isMenuOpen ? onMenuClose() : onMenuOpen());
|
menuHandler.toggle();
|
||||||
|
},
|
||||||
|
{ preventDefault: true },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'mod + ,',
|
||||||
|
() => {
|
||||||
|
if (isViewLocked || supressSettings) return;
|
||||||
|
showEditFormDrawer();
|
||||||
|
},
|
||||||
|
{ preventDefault: true },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
if (isViewLocked) {
|
if (isViewLocked) {
|
||||||
return <ViewLockedIcon />;
|
return <ViewLockedIcon />;
|
||||||
@@ -27,10 +41,11 @@ function ViewNavigationMenu() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
|
<FloatingNavigation
|
||||||
<NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />
|
toggleMenu={menuHandler.toggle}
|
||||||
|
toggleSettings={supressSettings ? undefined : () => showEditFormDrawer()}
|
||||||
|
/>
|
||||||
|
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(ViewNavigationMenu);
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.linkIcon {
|
||||||
|
margin-left: auto;
|
||||||
|
@include rotate-fourty-five;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { IoArrowUp } from 'react-icons/io5';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useElectronEvent } from '../../../hooks/useElectronEvent';
|
||||||
|
import { handleLinks } from '../../../utils/linkUtils';
|
||||||
|
import NavigationMenuItem from '../navigation-menu-item/NavigationMenuItem';
|
||||||
|
|
||||||
|
import style from './ClientLink.module.scss';
|
||||||
|
|
||||||
|
interface ClientLinkProps {
|
||||||
|
current: boolean;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClientLink({ current, to, children }: PropsWithChildren<ClientLinkProps>) {
|
||||||
|
const { isElectron } = useElectronEvent();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (isElectron) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuItem active={current} onClick={() => handleLinks(to)}>
|
||||||
|
{children}
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</NavigationMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationMenuItem active={current} onClick={() => navigate(`/${to}`)}>
|
||||||
|
{children}
|
||||||
|
</NavigationMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import { IoLockClosedOutline } from 'react-icons/io5';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useIsSmallDevice } from '../../../hooks/useIsSmallDevice';
|
||||||
|
import NavigationMenuItem from '../navigation-menu-item/NavigationMenuItem';
|
||||||
|
|
||||||
|
export default function EditorNavigation() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isSmallDevice = useIsSmallDevice();
|
||||||
|
|
||||||
|
if (!isSmallDevice) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuItem active={location.pathname === '/editor'} onClick={() => navigate('/editor')}>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
Editor
|
||||||
|
</NavigationMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavigationMenuItem active={location.pathname === '/timercontrol'} onClick={() => navigate('/timercontrol')}>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
Timer Controls
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem active={location.pathname === '/messagecontrol'} onClick={() => navigate('/messagecontrol')}>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
Message Controls
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem active={location.pathname === '/rundown'} onClick={() => navigate('/rundown')}>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
Rundown
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
.fadeable {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1rem;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: $zindex-nav;
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import { IoApps } from 'react-icons/io5';
|
||||||
|
import { IoSettingsOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { useFadeOutOnInactivity } from '../../../hooks/useFadeOutOnInactivity';
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
import IconButton from '../../buttons/IconButton';
|
||||||
|
|
||||||
|
import style from './FloatingNavigation.module.scss';
|
||||||
|
|
||||||
|
interface FloatingNavigationProps {
|
||||||
|
toggleMenu: () => void;
|
||||||
|
toggleSettings?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FloatingNavigation({ toggleMenu, toggleSettings }: FloatingNavigationProps) {
|
||||||
|
const isButtonShown = useFadeOutOnInactivity(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id='fadeable-navigation'
|
||||||
|
className={cx([style.fadeable, style.buttonContainer, !isButtonShown && style.hidden])}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant='subtle-white'
|
||||||
|
size='xlarge'
|
||||||
|
onClick={toggleMenu}
|
||||||
|
aria-label='toggle menu'
|
||||||
|
data-testid='navigation__toggle-menu'
|
||||||
|
>
|
||||||
|
<IoApps />
|
||||||
|
</IconButton>
|
||||||
|
{toggleSettings && (
|
||||||
|
<IconButton
|
||||||
|
variant='subtle-white'
|
||||||
|
size='xlarge'
|
||||||
|
onClick={toggleSettings}
|
||||||
|
aria-label='toggle settings'
|
||||||
|
data-testid='navigation__toggle-settings'
|
||||||
|
>
|
||||||
|
<IoSettingsOutline />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
.link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
color: $action-text-color;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition-property: color;
|
||||||
|
transition-duration: $transition-time-action;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $ontime-color;
|
||||||
|
background-color: $gray-1350;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $border-color-ondark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background-color: $gray-1300;
|
||||||
|
border: 2px solid $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
background-color: $gray-1300;
|
||||||
|
border-left: 4px solid $action-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { isKeyEnter } from '../../../utils/keyEvent';
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './NavigationMenuItem.module.scss';
|
||||||
|
|
||||||
|
interface NavigationMenuItemProps {
|
||||||
|
active?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavigationMenuItem({
|
||||||
|
active,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}: PropsWithChildren<NavigationMenuItemProps>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx([style.link, active && style.current, className])}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && onClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
.header {
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interfaces {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goIcon {
|
||||||
|
@include rotate-fourty-five;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { IoArrowUp } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import useInfo from '../../../hooks-query/useInfo';
|
||||||
|
import { linkToOtherHost, openLink } from '../../../utils/linkUtils';
|
||||||
|
import CopyTag from '../../copy-tag/CopyTag';
|
||||||
|
|
||||||
|
import style from './OtherAddresses.module.scss';
|
||||||
|
|
||||||
|
interface OtherAddressesProps {
|
||||||
|
currentLocation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OtherAddresses({ currentLocation }: OtherAddressesProps) {
|
||||||
|
const { data } = useInfo();
|
||||||
|
|
||||||
|
// there is no point showing this if we only have one interface
|
||||||
|
if (data.networkInterfaces.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={style.header}>Accessible on external networks</div>
|
||||||
|
<div className={style.interfaces}>
|
||||||
|
{data?.networkInterfaces?.map((nif) => {
|
||||||
|
if (nif.name === 'localhost') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = linkToOtherHost(nif.address, currentLocation);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CopyTag
|
||||||
|
key={nif.name}
|
||||||
|
copyValue={address}
|
||||||
|
onClick={() => openLink(address)}
|
||||||
|
label='Copy IP or navigate to address'
|
||||||
|
>
|
||||||
|
{nif.address} <IoArrowUp className={style.goIcon} />
|
||||||
|
</CopyTag>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
|
interface EditorVisibilityOptions {
|
||||||
|
isLockable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useViewEditor({ isLockable }: EditorVisibilityOptions) {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const showEditFormDrawer = useCallback(() => {
|
||||||
|
searchParams.set('edit', 'true');
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
const isViewLocked = useMemo(() => {
|
||||||
|
if (!isLockable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isStringBoolean(searchParams.get('locked'));
|
||||||
|
}, [isLockable, searchParams]);
|
||||||
|
|
||||||
|
return { showEditFormDrawer, isViewLocked };
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
.fadeable {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lockIcon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: $active-indicator;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: $zindex-nav;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { IoLockClosedOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { useFadeOutOnInactivity } from '../../../hooks/useFadeOutOnInactivity';
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './ViewLockedIcon.module.scss';
|
||||||
|
|
||||||
|
export default function ViewLockedIcon() {
|
||||||
|
const isLockIconShown = useFadeOutOnInactivity();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx([style.fadeable, style.lockIcon, !isLockIconShown && style.hidden])}>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
|
||||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
|
||||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
|
||||||
import { Playback } from 'ontime-types';
|
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
interface PlaybackIconProps {
|
|
||||||
state: Playback;
|
|
||||||
skipTooltip?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PlaybackIcon(props: PlaybackIconProps) {
|
|
||||||
const { state, skipTooltip, className } = props;
|
|
||||||
|
|
||||||
// if timer is Pause or Armed
|
|
||||||
let label = 'Timer Paused';
|
|
||||||
let Icon = IoPause;
|
|
||||||
|
|
||||||
if (state === Playback.Roll) {
|
|
||||||
label = 'Timer Rolling';
|
|
||||||
Icon = IoPlay;
|
|
||||||
} else if (state === Playback.Play) {
|
|
||||||
label = 'Timer Playing';
|
|
||||||
Icon = IoPlay;
|
|
||||||
} else if (state === Playback.Stop) {
|
|
||||||
label = 'Timer Stopped';
|
|
||||||
Icon = IoStop;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skipTooltip) {
|
|
||||||
return <Icon className={className} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
|
|
||||||
<Icon className={className} />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.popup {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
z-index: $zindex-dialog;
|
||||||
|
|
||||||
|
color: $ui-white;
|
||||||
|
background-color: $gray-1250;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: $box-shadow-l1;
|
||||||
|
border: 2px solid $gray-1200;
|
||||||
|
|
||||||
|
//width: 32rem;
|
||||||
|
max-width: 90vw;
|
||||||
|
|
||||||
|
transform-origin: var(--transform-origin);
|
||||||
|
transition:
|
||||||
|
transform 150ms,
|
||||||
|
opacity 150ms;
|
||||||
|
|
||||||
|
&[data-starting-style],
|
||||||
|
&[data-ending-style] {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { Popover } from '@base-ui-components/react/popover';
|
||||||
|
|
||||||
|
import style from './Popover.module.scss';
|
||||||
|
|
||||||
|
interface PopoverContentsProps extends Popover.Positioner.Props {
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
export default function PopoverContents({
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...popoverProps
|
||||||
|
}: PropsWithChildren<PopoverContentsProps>) {
|
||||||
|
return (
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Positioner sideOffset={8} {...popoverProps}>
|
||||||
|
<Popover.Popup className={style.popup}>
|
||||||
|
{title && <Popover.Title className={style.title}>{title}</Popover.Title>}
|
||||||
|
<Popover.Description className={className}>{children}</Popover.Description>
|
||||||
|
</Popover.Popup>
|
||||||
|
</Popover.Positioner>
|
||||||
|
</Popover.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,10 +9,6 @@ $progress-bar-br: 3px;
|
|||||||
border-radius: $progress-bar-br;
|
border-radius: $progress-bar-br;
|
||||||
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
&--hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar__indicator {
|
.progress-bar__indicator {
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ import './ProgressBar.scss';
|
|||||||
interface ProgressBarProps {
|
interface ProgressBarProps {
|
||||||
current: MaybeNumber;
|
current: MaybeNumber;
|
||||||
duration: MaybeNumber;
|
duration: MaybeNumber;
|
||||||
hidden?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProgressBar(props: ProgressBarProps) {
|
export default function ProgressBar(props: ProgressBarProps) {
|
||||||
const { current, duration, hidden, className = '' } = props;
|
const { current, duration, className } = props;
|
||||||
const progress = getProgress(current, duration);
|
const progress = getProgress(current, duration);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`progress-bar__bg ${hidden ? 'progress-bar__bg--hidden' : ''} ${className}`}>
|
<div className={`progress-bar__bg ${className}`}>
|
||||||
<div className='progress-bar__indicator' style={{ width: `${progress}%` }} />
|
<div className='progress-bar__indicator' style={{ width: `${progress}%` }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
import { IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
import { IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
||||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
|
||||||
|
|
||||||
import style from './ProtectRoute.module.scss';
|
import style from './ProtectRoute.module.scss';
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user