mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 11:23:50 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75cbcd8e85 | |||
| 701f24cece | |||
| 166160dda9 | |||
| 9c5116f197 | |||
| deb1f043c7 | |||
| cbba19a9b1 | |||
| a5daa76616 | |||
| 08b8e73393 | |||
| 2f3cf9825a | |||
| b0f02e726d | |||
| ac40292813 | |||
| 2ac00ee8cc | |||
| 972f9e35fd | |||
| 1d7ee163a0 | |||
| 1054ec0a5e | |||
| fc2e76db0a | |||
| ef3d541eb3 | |||
| 8daf3313f2 | |||
| 93c913ec5a | |||
| 71b2fa8e21 | |||
| f2b78e0f32 | |||
| b70ef80320 | |||
| 426d42ab63 | |||
| 242cefd35c | |||
| 6beda1575a | |||
| ba75d52d29 | |||
| 35a4361ae9 | |||
| 77225383a4 | |||
| f0e81c0cf0 | |||
| 9355d7bf27 | |||
| cd39227af7 | |||
| 14e21214af | |||
| 03184b0321 | |||
| cd5af769ab | |||
| d116670318 | |||
| 9ba5ebd4bd | |||
| 3f1f06f7c5 | |||
| a78586d2fa | |||
| 8e93b0af25 | |||
| 9a5d50d933 | |||
| 1189a94bff | |||
| f128f7acd2 | |||
| 80ec2d1186 | |||
| 18e6f0d7c4 | |||
| 48f2511764 |
@@ -103,6 +103,9 @@ jobs:
|
|||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: './apps/electron/dist/ontime-linux.AppImage'
|
files: |
|
||||||
|
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-arm64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-armv7l.AppImage
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ override.css
|
|||||||
|
|
||||||
# working stuff
|
# working stuff
|
||||||
**/TODO.md
|
**/TODO.md
|
||||||
|
**.local.**
|
||||||
|
|
||||||
# docker utils
|
# docker utils
|
||||||
ontime-db
|
ontime-db
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui-components/react": "1.0.0-beta.4",
|
"@base-ui/react": "1.0.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -17,21 +17,20 @@
|
|||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
|
||||||
"csv-stringify": "^6.6.0",
|
"csv-stringify": "^6.6.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.3",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.3",
|
||||||
"react-fast-compare": "^3.2.2",
|
"react-fast-compare": "^3.2.2",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"react-qr-code": "^2.0.18",
|
"react-qr-code": "^2.0.18",
|
||||||
"react-router": "^7.8.2",
|
"react-router": "^7.11.0",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"react-virtuoso": "^4.14.0",
|
"react-virtuoso": "^4.17.0",
|
||||||
"web-vitals": "^5.1.0",
|
"web-vitals": "^5.1.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
@@ -66,6 +65,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"@typescript-eslint/parser": "catalog:",
|
||||||
"@vitejs/plugin-react": "4.5.1",
|
"@vitejs/plugin-react": "4.5.1",
|
||||||
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"eslint-config-prettier": "catalog:",
|
"eslint-config-prettier": "catalog:",
|
||||||
"eslint-plugin-jest": "^28.6.0",
|
"eslint-plugin-jest": "^28.6.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BrowserRouter } from 'react-router';
|
import { BrowserRouter } from 'react-router';
|
||||||
import { Tooltip } from '@base-ui-components/react/tooltip';
|
import { Tooltip } from '@base-ui/react/tooltip';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,23 @@ export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
|||||||
*/
|
*/
|
||||||
export async function fetchCurrentRundown(): Promise<Rundown> {
|
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||||
const res = await axios.get(`${rundownPath}/current`);
|
const res = await axios.get(`${rundownPath}/current`);
|
||||||
|
if (!isValidRundown(res.data)) {
|
||||||
|
throw new Error('Invalid rundown payload');
|
||||||
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
||||||
|
function isValidRundown(x: any): x is Rundown {
|
||||||
|
return (
|
||||||
|
x &&
|
||||||
|
typeof x === 'object' &&
|
||||||
|
typeof x.id === 'string' &&
|
||||||
|
Array.isArray(x.order) &&
|
||||||
|
Array.isArray(x.flatOrder) &&
|
||||||
|
x.entries &&
|
||||||
|
typeof x.entries === 'object' &&
|
||||||
|
typeof x.revision === 'number'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +91,10 @@ export async function postAddEntry(
|
|||||||
/**
|
/**
|
||||||
* HTTP request to edit an entry
|
* HTTP request to edit an entry
|
||||||
*/
|
*/
|
||||||
export async function putEditEntry(rundownId: RundownId, data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
export async function putEditEntry(
|
||||||
|
rundownId: RundownId,
|
||||||
|
data: Partial<OntimeEntry>,
|
||||||
|
): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +126,11 @@ export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry
|
|||||||
/**
|
/**
|
||||||
* HTTP request to swap two events
|
* HTTP request to swap two events
|
||||||
*/
|
*/
|
||||||
export async function requestEventSwap(rundownId: RundownId, from: EntryId, to: EntryId): Promise<AxiosResponse<Rundown>> {
|
export async function requestEventSwap(
|
||||||
|
rundownId: RundownId,
|
||||||
|
from: EntryId,
|
||||||
|
to: EntryId,
|
||||||
|
): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +144,12 @@ export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId):
|
|||||||
/**
|
/**
|
||||||
* HTTP request for cloning an entry
|
* HTTP request for cloning an entry
|
||||||
*/
|
*/
|
||||||
export async function postCloneEntry(rundownId: RundownId, entryId: EntryId): Promise<AxiosResponse<Rundown>> {
|
export async function postCloneEntry(
|
||||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`);
|
rundownId: RundownId,
|
||||||
|
entryId: EntryId,
|
||||||
|
options?: { before?: EntryId; after?: EntryId },
|
||||||
|
): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
.subtle {
|
|
||||||
background: $gray-1050;
|
|
||||||
color: $blue-400;
|
|
||||||
line-height: 1em;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $blue-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtle-white {
|
|
||||||
background: $gray-1050;
|
|
||||||
color: $ui-white;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $ui-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background: $gray-1050;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
|
||||||
background: $blue-700;
|
|
||||||
color: $ui-white;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $blue-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $blue-800;
|
|
||||||
border-color: $blue-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: $viewer-opacity-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.destructive {
|
|
||||||
background: $red-700;
|
|
||||||
color: $ui-white;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $red-600;
|
|
||||||
color: $ui-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $red-800;
|
|
||||||
border-color: $red-900;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtle-destructive {
|
|
||||||
background: $gray-1050;
|
|
||||||
color: $red-400;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghosted {
|
|
||||||
background: transparent;
|
|
||||||
color: $blue-500;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $blue-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghosted-white {
|
|
||||||
background: transparent;
|
|
||||||
color: $ui-white;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $ui-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.ghosted-destructive {
|
|
||||||
background: transparent;
|
|
||||||
color: $red-500;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@use '@/theme/viewerDefs' as *;
|
@use './buttonVariants' as base;
|
||||||
@import './BaseButtonStyles.module.scss';
|
|
||||||
|
@include base.button-variants;
|
||||||
|
|
||||||
.baseButton {
|
.baseButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -81,4 +82,3 @@
|
|||||||
.fluid {
|
.fluid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@use '@/theme/viewerDefs' as *;
|
@use './buttonVariants' as base;
|
||||||
@import './BaseButtonStyles.module.scss';
|
|
||||||
|
@include base.button-variants;
|
||||||
|
|
||||||
.baseIconButton {
|
.baseIconButton {
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
@use '@/theme/themeTokens' as *;
|
||||||
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
@mixin button-variants {
|
||||||
|
.subtle {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $blue-400;
|
||||||
|
line-height: 1em;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-white {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: $gray-1050;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: $blue-700;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $blue-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $blue-800;
|
||||||
|
border-color: $blue-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $viewer-opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.destructive {
|
||||||
|
background: $red-700;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $red-600;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $red-800;
|
||||||
|
border-color: $red-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-destructive {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $red-400;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted {
|
||||||
|
background: transparent;
|
||||||
|
color: $blue-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted-white {
|
||||||
|
background: transparent;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted-destructive {
|
||||||
|
background: transparent;
|
||||||
|
color: $red-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IoCheckmark } from 'react-icons/io5';
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
|
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
||||||
|
|
||||||
import style from './Checkbox.module.scss';
|
import style from './Checkbox.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { navigatorConstants } from '../../../viewerConfig';
|
|||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
|
import Dialog from '../dialog/Dialog';
|
||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
import Input from '../input/input/Input';
|
import Input from '../input/input/Input';
|
||||||
import AppLink from '../link/app-link/AppLink';
|
import AppLink from '../link/app-link/AppLink';
|
||||||
import Modal from '../modal/Modal';
|
|
||||||
import Select from '../select/Select';
|
import Select from '../select/Select';
|
||||||
|
|
||||||
import style from './RedirectClientModal.module.scss';
|
import style from './RedirectClientModal.module.scss';
|
||||||
@@ -57,7 +57,7 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Dialog
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
showCloseButton
|
showCloseButton
|
||||||
@@ -98,7 +98,10 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
fluid
|
fluid
|
||||||
options={viewOptions}
|
options={viewOptions}
|
||||||
defaultValue={viewOptions[0].value}
|
defaultValue={viewOptions[0].value}
|
||||||
onValueChange={(value) => setSelected(value)}
|
onValueChange={(value) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setSelected(value);
|
||||||
|
}}
|
||||||
disabled={enabledPresets.length === 0}
|
disabled={enabledPresets.length === 0}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PropsWithChildren, useRef, useState } from 'react';
|
import { PropsWithChildren, useRef, useState } from 'react';
|
||||||
import { IoCheckmark, IoCopy } from 'react-icons/io5';
|
import { IoCheckmark, IoCopy } from 'react-icons/io5';
|
||||||
|
|
||||||
import copyToClipboard from '../../utils/copyToClipboard';
|
import { copyToClipboard } from '../../utils/copyToClipboard';
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
import IconButton from '../buttons/IconButton';
|
import IconButton from '../buttons/IconButton';
|
||||||
@@ -25,15 +25,19 @@ export default function CopyTag({
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = async () => {
|
||||||
copyToClipboard(copyValue);
|
try {
|
||||||
setCopied(true);
|
await copyToClipboard(copyValue);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
// reset copied state
|
// reset copied state
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
}
|
}
|
||||||
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
min-width: min(420px, 90vw);
|
min-width: min(600px, 90vw);
|
||||||
|
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||||
|
|
||||||
import IconButton from '../buttons/IconButton';
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ interface DialogProps {
|
|||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
showBackdrop?: boolean;
|
showBackdrop?: boolean;
|
||||||
bodyElements: ReactNode;
|
bodyElements: ReactNode;
|
||||||
footerElements: ReactNode;
|
footerElements?: ReactNode;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { IconType } from 'react-icons';
|
import { IconType } from 'react-icons';
|
||||||
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
||||||
|
|
||||||
import style from './DropdownMenu.module.scss';
|
import style from './DropdownMenu.module.scss';
|
||||||
|
|
||||||
@@ -31,7 +31,13 @@ export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChil
|
|||||||
return <BaseMenu.Separator key={index} className={style.separator} />;
|
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled} data-type={item.type}>
|
<BaseMenu.Item
|
||||||
|
key={index}
|
||||||
|
className={style.item}
|
||||||
|
onClick={item.onClick}
|
||||||
|
disabled={item.disabled}
|
||||||
|
data-type={item.type}
|
||||||
|
>
|
||||||
{item.icon && <item.icon />}
|
{item.icon && <item.icon />}
|
||||||
{item.label}
|
{item.label}
|
||||||
</BaseMenu.Item>
|
</BaseMenu.Item>
|
||||||
|
|||||||
@@ -59,3 +59,10 @@
|
|||||||
height: 0.75em;
|
height: 0.75em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
border-radius: $panel-border-radius;
|
||||||
|
background-color: $bg-container-l2;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,3 +55,7 @@ interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
|
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
|
||||||
return <div className={cx([style.separator, style[orientation], className])} role='separator' {...elementProps} />;
|
return <div className={cx([style.separator, style[orientation], className])} role='separator' {...elementProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Panel({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cx([style.panel, className])} {...props} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Radio } from '@base-ui-components/react/radio';
|
import { Radio } from '@base-ui/react/radio';
|
||||||
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
||||||
|
|
||||||
import style from './BlockRadio.module.scss';
|
import style from './BlockRadio.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
||||||
import { Popover } from '@base-ui-components/react/popover';
|
import { Popover } from '@base-ui/react/popover';
|
||||||
|
|
||||||
import PopoverContents from '../../popover/Popover';
|
import PopoverContents from '../../popover/Popover';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||||
|
|
||||||
import IconButton from '../buttons/IconButton';
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export default function Modal({
|
|||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
if (!isOpen) onClose();
|
if (!isOpen) onClose();
|
||||||
}}
|
}}
|
||||||
dismissible={false}
|
disablePointerDismissal
|
||||||
>
|
>
|
||||||
<BaseDialog.Portal>
|
<BaseDialog.Portal>
|
||||||
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { memo } from 'react';
|
|||||||
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||||
import { LuCoffee } from 'react-icons/lu';
|
import { LuCoffee } from 'react-icons/lu';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { Dialog } from '@base-ui-components/react/dialog';
|
import { Dialog } from '@base-ui/react/dialog';
|
||||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||||
|
|
||||||
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
||||||
import { useKeepAwakeOptions } from '../../../features/keep-awake/KeepAwake';
|
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
@@ -69,7 +69,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
|||||||
<IoSwapVertical />
|
<IoSwapVertical />
|
||||||
{mirror && <span className={style.note}>Active</span>}
|
{mirror && <span className={style.note}>Active</span>}
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
{window.isSecureContext && (
|
{canUseWakeLock && (
|
||||||
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
||||||
Keep Awake
|
Keep Awake
|
||||||
<LuCoffee />
|
<LuCoffee />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { Popover } from '@base-ui-components/react/popover';
|
import { Popover } from '@base-ui/react/popover';
|
||||||
|
|
||||||
import style from './Popover.module.scss';
|
import style from './Popover.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Radio } from '@base-ui-components/react/radio';
|
import { Radio } from '@base-ui/react/radio';
|
||||||
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IoCheckmark } from 'react-icons/io5';
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
import { LuChevronsUpDown } from 'react-icons/lu';
|
import { LuChevronsUpDown } from 'react-icons/lu';
|
||||||
import { Select as BaseSelect } from '@base-ui-components/react/select';
|
import { Select as BaseSelect } from '@base-ui/react/select';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
|
import { Switch as BaseSwitch } from '@base-ui/react/switch';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
|
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
||||||
|
|
||||||
import style from './Tooltip.module.scss';
|
import style from './Tooltip.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ComponentProps, useEffect, useState } from 'react';
|
import { ComponentProps, useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../../views/common/viewUtils';
|
||||||
import Checkbox from '../checkbox/Checkbox';
|
import Checkbox from '../checkbox/Checkbox';
|
||||||
import Input from '../input/input/Input';
|
import Input from '../input/input/Input';
|
||||||
import Select, { SelectOption } from '../select/Select';
|
import Select, { SelectOption } from '../select/Select';
|
||||||
@@ -160,7 +160,16 @@ function ControlledSelect({ id, initialValue, options }: ControlledSelectProps)
|
|||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select size='large' name={id} options={options} value={selected} onValueChange={(value) => setSelected(value)} />
|
<Select
|
||||||
|
size='large'
|
||||||
|
name={id}
|
||||||
|
options={options}
|
||||||
|
value={selected}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setSelected(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FormEvent, memo } from 'react';
|
import { FormEvent, memo } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
import { Dialog } from '@base-ui-components/react/dialog';
|
import { Dialog } from '@base-ui/react/dialog';
|
||||||
import { OntimeView } from 'ontime-types';
|
import { OntimeView } from 'ontime-types';
|
||||||
|
|
||||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
|
import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
@@ -8,8 +8,6 @@ import { fetchCurrentRundown } from '../api/rundown';
|
|||||||
import { useSelectedEventId } from '../hooks/useSocket';
|
import { useSelectedEventId } from '../hooks/useSocket';
|
||||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||||
|
|
||||||
import useProjectData from './useProjectData';
|
|
||||||
|
|
||||||
// revision is -1 so that the remote revision is higher
|
// revision is -1 so that the remote revision is higher
|
||||||
const cachedRundownPlaceholder: Rundown = {
|
const cachedRundownPlaceholder: Rundown = {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
@@ -27,9 +25,9 @@ export default function useRundown() {
|
|||||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||||
queryKey: RUNDOWN,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchCurrentRundown,
|
queryFn: fetchCurrentRundown,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,30 +44,13 @@ export function useRundownWithMetadata() {
|
|||||||
*/
|
*/
|
||||||
export function useFlatRundown() {
|
export function useFlatRundown() {
|
||||||
const { data, status } = useRundown();
|
const { data, status } = useRundown();
|
||||||
const { data: projectData } = useProjectData();
|
|
||||||
|
|
||||||
const loadedProject = useRef<string>('');
|
const flatRundown = useMemo(() => {
|
||||||
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
if (data.revision === -1) {
|
||||||
const [flatRundown, setFlatRundown] = useState<OntimeEntry[]>([]);
|
return [];
|
||||||
|
|
||||||
// update data whenever the revision changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (data.revision !== -1 || data.revision !== prevRevision) {
|
|
||||||
const flatRundown = data.flatOrder.map((id) => data.entries[id]);
|
|
||||||
setFlatRundown(flatRundown);
|
|
||||||
setPrevRevision(data.revision);
|
|
||||||
}
|
}
|
||||||
}, [data.entries, data.flatOrder, data.revision, prevRevision]);
|
return data.flatOrder.map((id) => data.entries[id]).filter((entry): entry is OntimeEntry => entry !== undefined);
|
||||||
|
}, [data]);
|
||||||
// TODO: should we have a project id field?
|
|
||||||
// TODO(v4): cleanup as part of load multiple rundowns
|
|
||||||
// invalidate current version if project changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectData?.title !== loadedProject.current) {
|
|
||||||
setPrevRevision(-1);
|
|
||||||
loadedProject.current = projectData?.title ?? '';
|
|
||||||
}
|
|
||||||
}, [projectData]);
|
|
||||||
|
|
||||||
return { data: flatRundown, rundownId: data.id, status };
|
return { data: flatRundown, rundownId: data.id, status };
|
||||||
}
|
}
|
||||||
@@ -84,6 +65,10 @@ export function useFlatRundownWithMetadata() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to a partial rundown based on a filter callback
|
* Provides access to a partial rundown based on a filter callback
|
||||||
|
*
|
||||||
|
* Callers MUST memoize the callback with useCallback to prevent
|
||||||
|
* re-filtering on every render.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
||||||
const { data, status } = useFlatRundownWithMetadata();
|
const { data, status } = useFlatRundownWithMetadata();
|
||||||
@@ -100,11 +85,6 @@ export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boo
|
|||||||
export function useEntry(entryId: EntryId | null): OntimeEntry | null {
|
export function useEntry(entryId: EntryId | null): OntimeEntry | null {
|
||||||
const { data: rundown } = useRundown();
|
const { data: rundown } = useRundown();
|
||||||
|
|
||||||
// track the specific entry we care about
|
if (entryId === null) return null;
|
||||||
const entry = useMemo(() => {
|
return rundown.entries[entryId] ?? null;
|
||||||
if (entryId === null) return null;
|
|
||||||
return rundown.entries[entryId];
|
|
||||||
}, [entryId, rundown.entries]);
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
EntryId,
|
EntryId,
|
||||||
|
InsertOptions,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isOntimeGroup,
|
isOntimeGroup,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
@@ -173,7 +174,8 @@ export const useEntryActions = () => {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: cloneEntryMutation } = useMutation({
|
const { mutateAsync: cloneEntryMutation } = useMutation({
|
||||||
mutationFn: ([rundownId, entryId]: Parameters<typeof postCloneEntry>) => postCloneEntry(rundownId, entryId),
|
mutationFn: ([rundownId, entryId, options]: Parameters<typeof postCloneEntry>) =>
|
||||||
|
postCloneEntry(rundownId, entryId, options),
|
||||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
});
|
});
|
||||||
@@ -182,14 +184,14 @@ export const useEntryActions = () => {
|
|||||||
* Clone an entry
|
* Clone an entry
|
||||||
*/
|
*/
|
||||||
const clone = useCallback(
|
const clone = useCallback(
|
||||||
async (entryId: EntryId) => {
|
async (entryId: EntryId, options?: InsertOptions) => {
|
||||||
try {
|
try {
|
||||||
const rundownId = getCurrentRundownData()?.id;
|
const rundownId = getCurrentRundownData()?.id;
|
||||||
if (!rundownId) {
|
if (!rundownId) {
|
||||||
throw new Error('Rundown not initialised');
|
throw new Error('Rundown not initialised');
|
||||||
}
|
}
|
||||||
|
|
||||||
await cloneEntryMutation([rundownId, entryId]);
|
await cloneEntryMutation([rundownId, entryId, options]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error cloning entry', error);
|
logAxiosError('Error cloning entry', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEntry, TimerType, TimeStrategy } from 'ontime-types';
|
|
||||||
|
|
||||||
import { cloneEvent } from '../clone';
|
|
||||||
|
|
||||||
describe('cloneEvent()', () => {
|
|
||||||
it('creates a stem from a given event', () => {
|
|
||||||
const original: OntimeEvent = {
|
|
||||||
id: 'unique',
|
|
||||||
type: SupportedEntry.Event,
|
|
||||||
flag: false,
|
|
||||||
title: 'title',
|
|
||||||
cue: 'cue',
|
|
||||||
note: 'note',
|
|
||||||
timeStart: 0,
|
|
||||||
duration: 10,
|
|
||||||
timeEnd: 10,
|
|
||||||
timerType: TimerType.CountDown,
|
|
||||||
timeStrategy: TimeStrategy.LockEnd,
|
|
||||||
parent: 'test',
|
|
||||||
linkStart: false,
|
|
||||||
countToEnd: false,
|
|
||||||
endAction: EndAction.None,
|
|
||||||
skip: false,
|
|
||||||
colour: 'F00',
|
|
||||||
revision: 10,
|
|
||||||
timeWarning: 120000,
|
|
||||||
timeDanger: 60000,
|
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
gap: 0,
|
|
||||||
triggers: [],
|
|
||||||
custom: {
|
|
||||||
lighting: '3',
|
|
||||||
} as EntryCustomFields,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cloned = cloneEvent(original);
|
|
||||||
expect(cloned).not.toBe(original);
|
|
||||||
expect(cloned.custom).not.toBe(original.custom);
|
|
||||||
expect(cloned.triggers).not.toBe(original.triggers);
|
|
||||||
|
|
||||||
expect(cloned).toMatchObject({
|
|
||||||
type: SupportedEntry.Event,
|
|
||||||
flag: original.flag,
|
|
||||||
title: original.title,
|
|
||||||
note: original.note,
|
|
||||||
timeStart: original.timeStart,
|
|
||||||
duration: original.duration,
|
|
||||||
timeEnd: original.timeEnd,
|
|
||||||
timerType: original.timerType,
|
|
||||||
timeStrategy: original.timeStrategy,
|
|
||||||
parent: 'test',
|
|
||||||
countToEnd: original.countToEnd,
|
|
||||||
linkStart: original.linkStart,
|
|
||||||
endAction: original.endAction,
|
|
||||||
skip: original.skip,
|
|
||||||
colour: original.colour,
|
|
||||||
revision: 0,
|
|
||||||
delay: original.delay,
|
|
||||||
dayOffset: original.dayOffset,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: original.timeWarning,
|
|
||||||
timeDanger: original.timeDanger,
|
|
||||||
triggers: original.triggers,
|
|
||||||
custom: original.custom,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { OntimeEvent, SupportedEntry } from 'ontime-types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Creates a safe duplicate of an event
|
|
||||||
* @param {OntimeEvent} event
|
|
||||||
* @param {string} [after]
|
|
||||||
* @return {OntimeEvent} clean event
|
|
||||||
*/
|
|
||||||
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
|
||||||
export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
|
||||||
return {
|
|
||||||
type: SupportedEntry.Event,
|
|
||||||
flag: event.flag,
|
|
||||||
title: event.title,
|
|
||||||
note: event.note,
|
|
||||||
timeStart: event.timeStart,
|
|
||||||
duration: event.duration,
|
|
||||||
timeEnd: event.timeEnd,
|
|
||||||
timerType: event.timerType,
|
|
||||||
timeStrategy: event.timeStrategy,
|
|
||||||
countToEnd: event.countToEnd,
|
|
||||||
linkStart: event.linkStart,
|
|
||||||
endAction: event.endAction,
|
|
||||||
skip: event.skip,
|
|
||||||
colour: event.colour,
|
|
||||||
parent: event.parent,
|
|
||||||
revision: 0,
|
|
||||||
delay: event.delay, // the events will be collocated, so having the same metadata is a good start
|
|
||||||
dayOffset: event.dayOffset,
|
|
||||||
gap: 0,
|
|
||||||
timeWarning: event.timeWarning,
|
|
||||||
timeDanger: event.timeDanger,
|
|
||||||
triggers: structuredClone(event.triggers),
|
|
||||||
custom: structuredClone(event.custom),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,18 @@
|
|||||||
// we need to this as a promise because safari
|
/**
|
||||||
export default async function copyToClipboard(text: string) {
|
* copy text to clipboard
|
||||||
setTimeout(async () => await navigator.clipboard?.writeText(text));
|
* @throws if not supported or permission denied
|
||||||
|
*/
|
||||||
|
export async function copyToClipboard(text: string) {
|
||||||
|
await navigator.clipboard?.writeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy to clipboard but safely ignore errors
|
||||||
|
*/
|
||||||
|
export async function safeCopyToClipboard(text: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await copyToClipboard(text);
|
||||||
|
} catch {
|
||||||
|
// Silently ignore errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,10 +230,18 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
|||||||
<div key={key} className={style.filterSection}>
|
<div key={key} className={style.filterSection}>
|
||||||
<label>
|
<label>
|
||||||
Runtime data source
|
Runtime data source
|
||||||
<Select
|
<Select<string | null>
|
||||||
value={watch(`filters.${index}.field`)}
|
// need to normalize '' to null for the Select to show the placeholder
|
||||||
onValueChange={(value) => setValue(`filters.${index}.field`, value, { shouldDirty: true })}
|
value={watch(`filters.${index}.field`) || null}
|
||||||
options={fieldList.map(({ value, label }) => ({ value, label }))}
|
onValueChange={(value) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue(`filters.${index}.field`, value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
options={fieldList.map(({ value, label }) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
disabled: value === null,
|
||||||
|
}))}
|
||||||
aria-label='Event field'
|
aria-label='Event field'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
||||||
@@ -242,11 +250,14 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
|||||||
Matching condition
|
Matching condition
|
||||||
<Select
|
<Select
|
||||||
value={watch(`filters.${index}.operator`)}
|
value={watch(`filters.${index}.operator`)}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value: string | null) => {
|
||||||
setValue(`filters.${index}.operator`, value as 'equals' | 'not_equals' | 'contains', {
|
if (value === null) return;
|
||||||
shouldDirty: true,
|
setValue(
|
||||||
})
|
`filters.${index}.operator`,
|
||||||
}
|
value as 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'not_contains',
|
||||||
|
{ shouldDirty: true },
|
||||||
|
);
|
||||||
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'equals', label: 'equals' },
|
{ value: 'equals', label: 'equals' },
|
||||||
{ value: 'not_equals', label: 'not equals' },
|
{ value: 'not_equals', label: 'not equals' },
|
||||||
@@ -282,7 +293,8 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
|||||||
<div className={style.innerColumn}>
|
<div className={style.innerColumn}>
|
||||||
<h3>Outputs</h3>
|
<h3>Outputs</h3>
|
||||||
<Info>
|
<Info>
|
||||||
Automation outputs can be used to send data from Ontime to external software.
|
Automation outputs can be used to send data from Ontime to external software <br />
|
||||||
|
or to change properties of Ontime itself.
|
||||||
<ExternalLink href={integrationsDocsUrl}>See the documentation for templates</ExternalLink>
|
<ExternalLink href={integrationsDocsUrl}>See the documentation for templates</ExternalLink>
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
|
|||||||
+19
-10
@@ -44,8 +44,9 @@ export default function OntimeActionForm({
|
|||||||
<label>
|
<label>
|
||||||
Action
|
Action
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={(value: OntimeActionKey | null) => {
|
||||||
handleSetAction(value as OntimeActionKey);
|
if (value === null) return;
|
||||||
|
handleSetAction(value);
|
||||||
}}
|
}}
|
||||||
value={watch(`outputs.${index}.action`)}
|
value={watch(`outputs.${index}.action`)}
|
||||||
options={[
|
options={[
|
||||||
@@ -97,13 +98,13 @@ export default function OntimeActionForm({
|
|||||||
Visibility
|
Visibility
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
// we need to translate the undefined value to 'untouched'
|
// we need to translate the null to undefined so it becomes 'untouched'
|
||||||
const translatedValue = value === 'untouched' ? undefined : (value as boolean | undefined);
|
const translatedValue = value === null ? undefined : value;
|
||||||
setValue(`outputs.${index}.visible`, translatedValue, { shouldDirty: true });
|
setValue(`outputs.${index}.visible`, translatedValue, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
value={watch(`outputs.${index}.visible`) === undefined ? 'untouched' : watch(`outputs.${index}.visible`)}
|
value={watch(`outputs.${index}.visible`)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'untouched', label: 'Untouched' },
|
{ value: null, label: 'Untouched' },
|
||||||
{ value: true, label: 'Show' },
|
{ value: true, label: 'Show' },
|
||||||
{ value: false, label: 'Hide' },
|
{ value: false, label: 'Hide' },
|
||||||
]}
|
]}
|
||||||
@@ -116,9 +117,16 @@ export default function OntimeActionForm({
|
|||||||
{selectedAction === 'message-secondary' && (
|
{selectedAction === 'message-secondary' && (
|
||||||
<label>
|
<label>
|
||||||
Timer secondary source
|
Timer secondary source
|
||||||
<Select
|
<Select<SecondarySource | 'null' | null>
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setValue(`outputs.${index}.secondarySource`, value as SecondarySource, { shouldDirty: true });
|
// null -> no selection
|
||||||
|
if (value === null) return;
|
||||||
|
// 'null' -> clear the secondary source
|
||||||
|
if (value === 'null') {
|
||||||
|
setValue(`outputs.${index}.secondarySource`, null, { shouldDirty: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue(`outputs.${index}.secondarySource`, value, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
value={watch(`outputs.${index}.secondarySource`)}
|
value={watch(`outputs.${index}.secondarySource`)}
|
||||||
options={[
|
options={[
|
||||||
@@ -126,13 +134,14 @@ export default function OntimeActionForm({
|
|||||||
{ value: 'aux1', label: 'Auxiliary timer 1' },
|
{ value: 'aux1', label: 'Auxiliary timer 1' },
|
||||||
{ value: 'aux2', label: 'Auxiliary timer 2' },
|
{ value: 'aux2', label: 'Auxiliary timer 2' },
|
||||||
{ value: 'aux3', label: 'Auxiliary timer 3' },
|
{ value: 'aux3', label: 'Auxiliary timer 3' },
|
||||||
{ value: 'external', label: 'External' },
|
{ value: 'secondary', label: 'Secondary' },
|
||||||
{ value: 'null', label: 'None' },
|
{ value: 'null', label: 'None' }, // allow the user to clear the secondary source
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={style.test}>{children}</div>
|
<div className={style.test}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -106,7 +106,10 @@ export default function TriggerForm({
|
|||||||
Lifecycle trigger
|
Lifecycle trigger
|
||||||
<Select
|
<Select
|
||||||
value={watch('trigger')}
|
value={watch('trigger')}
|
||||||
onValueChange={(value) => setValue('trigger', value as TimerLifeCycle, { shouldDirty: true })}
|
onValueChange={(value) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('trigger', value as TimerLifeCycle, { shouldDirty: true });
|
||||||
|
}}
|
||||||
options={cycles.map((cycle) => ({ value: cycle.value, label: cycle.label }))}
|
options={cycles.map((cycle) => ({ value: cycle.value, label: cycle.label }))}
|
||||||
aria-label='Lifecycle trigger'
|
aria-label='Lifecycle trigger'
|
||||||
/>
|
/>
|
||||||
@@ -116,7 +119,10 @@ export default function TriggerForm({
|
|||||||
Automation title
|
Automation title
|
||||||
<Select
|
<Select
|
||||||
value={watch('automationId')}
|
value={watch('automationId')}
|
||||||
onValueChange={(value) => setValue('automationId', value, { shouldDirty: true })}
|
onValueChange={(value: string | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('automationId', value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
options={automationSelect}
|
options={automationSelect}
|
||||||
aria-label='Automation title'
|
aria-label='Automation title'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function isAutomation(automation: AutomationDTO | Automation): automation
|
|||||||
}
|
}
|
||||||
|
|
||||||
const staticSelectProperties = [
|
const staticSelectProperties = [
|
||||||
|
{ value: null, label: 'Select field' },
|
||||||
{ value: 'eventNow.id', label: 'ID' },
|
{ value: 'eventNow.id', label: 'ID' },
|
||||||
{ value: 'eventNow.title', label: 'Title' },
|
{ value: 'eventNow.title', label: 'Title' },
|
||||||
{ value: 'eventNow.cue', label: 'Cue' },
|
{ value: 'eventNow.cue', label: 'Cue' },
|
||||||
@@ -42,7 +43,7 @@ const staticNextSelectProperties = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type SelectableField = {
|
type SelectableField = {
|
||||||
value: string; // string encodes path in runtime state object
|
value: string | null; // string encodes path in runtime state object
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -142,7 +142,10 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
|
|||||||
options={targetOptions}
|
options={targetOptions}
|
||||||
{...register('target', { required: 'Target is required' })}
|
{...register('target', { required: 'Target is required' })}
|
||||||
value={watch('target')}
|
value={watch('target')}
|
||||||
onValueChange={(value) => setValue('target', value, { shouldDirty: true })}
|
onValueChange={(value: OntimeViewPresettable | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('target', value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+12
-3
@@ -51,7 +51,10 @@ export default function RundownDefaultSettings() {
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={defaultTimeStrategy}
|
value={defaultTimeStrategy}
|
||||||
onValueChange={(value) => setTimeStrategy(value as TimeStrategy)}
|
onValueChange={(value: TimeStrategy | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setTimeStrategy(value);
|
||||||
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: TimeStrategy.LockDuration, label: 'Duration' },
|
{ value: TimeStrategy.LockDuration, label: 'Duration' },
|
||||||
{ value: TimeStrategy.LockEnd, label: 'End Time' },
|
{ value: TimeStrategy.LockEnd, label: 'End Time' },
|
||||||
@@ -73,7 +76,10 @@ export default function RundownDefaultSettings() {
|
|||||||
<Panel.Field title='Timer type' description='Default type of timer for new events' />
|
<Panel.Field title='Timer type' description='Default type of timer for new events' />
|
||||||
<Select
|
<Select
|
||||||
value={defaultTimerType}
|
value={defaultTimerType}
|
||||||
onValueChange={(value) => setDefaultTimerType(value as TimerType)}
|
onValueChange={(value: TimerType | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setDefaultTimerType(value);
|
||||||
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: TimerType.CountDown, label: 'Count down' },
|
{ value: TimerType.CountDown, label: 'Count down' },
|
||||||
{ value: TimerType.CountUp, label: 'Count up' },
|
{ value: TimerType.CountUp, label: 'Count up' },
|
||||||
@@ -86,7 +92,10 @@ export default function RundownDefaultSettings() {
|
|||||||
<Panel.Field title='End Action' description='Default end action for new events' />
|
<Panel.Field title='End Action' description='Default end action for new events' />
|
||||||
<Select
|
<Select
|
||||||
value={defaultEndAction}
|
value={defaultEndAction}
|
||||||
onValueChange={(value) => setDefaultEndAction(value as EndAction)}
|
onValueChange={(value: EndAction | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setDefaultEndAction(value);
|
||||||
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: EndAction.None, label: 'None' },
|
{ value: EndAction.None, label: 'None' },
|
||||||
{ value: EndAction.LoadNext, label: 'Load next' },
|
{ value: EndAction.LoadNext, label: 'Load next' },
|
||||||
|
|||||||
+4
-3
@@ -162,9 +162,10 @@ export default function ImportMapForm({
|
|||||||
<Select
|
<Select
|
||||||
id={importName as string}
|
id={importName as string}
|
||||||
value={watch(label as keyof NamedImportMap) as string}
|
value={watch(label as keyof NamedImportMap) as string}
|
||||||
onValueChange={(value: string) =>
|
onValueChange={(value: string | null) => {
|
||||||
setValue(label as keyof NamedImportMap, value, { shouldDirty: true })
|
if (value === null) return;
|
||||||
}
|
setValue(label as keyof NamedImportMap, value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
options={worksheetNames?.map((name) => ({ value: name, label: name })) || []}
|
options={worksheetNames?.map((name) => ({ value: name, label: name })) || []}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -153,7 +153,10 @@ export default function GeneralSettings() {
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={watch('timeFormat')}
|
value={watch('timeFormat')}
|
||||||
onValueChange={(value) => setValue('timeFormat', value as '12' | '24', { shouldDirty: true })}
|
onValueChange={(value: '12' | '24' | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('timeFormat', value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
defaultValue='24'
|
defaultValue='24'
|
||||||
options={[
|
options={[
|
||||||
{ value: '12', label: '12 hours 11:00:10 PM' },
|
{ value: '12', label: '12 hours 11:00:10 PM' },
|
||||||
@@ -169,7 +172,10 @@ export default function GeneralSettings() {
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={watch('language')}
|
value={watch('language')}
|
||||||
onValueChange={(value) => setValue('language', value, { shouldDirty: true })}
|
onValueChange={(value: string | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('language', value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
disabled={disableInputs}
|
disabled={disableInputs}
|
||||||
defaultValue='en'
|
defaultValue='en'
|
||||||
options={[
|
options={[
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={watch('settings.timeFormat')}
|
value={watch('settings.timeFormat')}
|
||||||
onValueChange={(value: '12' | '24') => setValue('settings.timeFormat', value, { shouldDirty: true })}
|
onValueChange={(value: '12' | '24' | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('settings.timeFormat', value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
defaultValue='24'
|
defaultValue='24'
|
||||||
options={[
|
options={[
|
||||||
{ value: '12', label: '12 hours 11:00:10 PM' },
|
{ value: '12', label: '12 hours 11:00:10 PM' },
|
||||||
@@ -91,7 +94,10 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={watch('settings.language')}
|
value={watch('settings.language')}
|
||||||
onValueChange={(value: string) => setValue('settings.language', value, { shouldDirty: true })}
|
onValueChange={(value: string | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('settings.language', value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
defaultValue='en'
|
defaultValue='en'
|
||||||
options={[
|
options={[
|
||||||
{ value: 'en', label: 'English' },
|
{ value: 'en', label: 'English' },
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import "./MessageControl.module.scss";
|
|
||||||
|
|
||||||
.inputItems {
|
.inputItems {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
@@ -8,5 +6,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@extend .label;
|
font-size: $inner-section-text-size;
|
||||||
|
color: $label-gray;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $action-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function InputRow(props: PropsWithChildren<InputRowProps>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.inputRow}>
|
<div>
|
||||||
<label className={cx([style.label, visible ?? style.active])} htmlFor={label}>
|
<label className={cx([style.label, visible ?? style.active])} htmlFor={label}>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.growPanel {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLayout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $section-spacing;
|
||||||
|
color: $ui-white;
|
||||||
|
padding-top: $panel-top-padding;
|
||||||
|
}
|
||||||
@@ -1,34 +1,32 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { CornerExtract } from '../../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||||
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
||||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
|
||||||
import { getIsNavigationLocked } from '../../../externals';
|
import { getIsNavigationLocked } from '../../../externals';
|
||||||
|
|
||||||
import MessageControl from './MessageControl';
|
import MessageControl from './MessageControl';
|
||||||
|
|
||||||
import style from '../../../views/editor/Editor.module.scss';
|
import style from './MessageControlExport.module.scss';
|
||||||
|
|
||||||
export default memo(MessageControlExport);
|
export default memo(MessageControlExport);
|
||||||
function MessageControlExport() {
|
function MessageControlExport() {
|
||||||
const isExtracted = window.location.pathname.includes('/messagecontrol');
|
const isExtracted = window.location.pathname.includes('/messagecontrol');
|
||||||
const classes = cx([style.content, style.contentColumnLayout]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectRoute permission='editor'>
|
<ProtectRoute permission='editor'>
|
||||||
<div className={style.messages} data-testid='panel-messages-control'>
|
<Editor.Panel className={style.growPanel} data-testid='panel-messages-control'>
|
||||||
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('messagecontrol', event)} />}
|
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('messagecontrol', event)} />}
|
||||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||||
|
|
||||||
<div className={classes}>
|
<div className={style.contentLayout}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MessageControl />
|
<MessageControl />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Editor.Panel>
|
||||||
</ProtectRoute>
|
</ProtectRoute>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-31
@@ -1,18 +1,3 @@
|
|||||||
.label {
|
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
color: $label-gray;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: $action-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewContainer {
|
|
||||||
display: grid;
|
|
||||||
gap: $element-spacing;
|
|
||||||
grid-template-columns: 3fr 2fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
background-color: $ui-black;
|
background-color: $ui-black;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -21,21 +6,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $element-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventStatus {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
margin: 0.5rem 0.25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainContent {
|
.mainContent {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -61,9 +31,13 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timerIndicators {
|
.eventStatus {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
margin: 0.5rem 0.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
@@ -73,3 +47,4 @@
|
|||||||
color: $active-indicator;
|
color: $active-indicator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import { handleLinks } from '../../../common/utils/linkUtils';
|
|||||||
import { cx, timerPlaceholder } from '../../../common/utils/styleUtils';
|
import { cx, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
import PipRoot from '../../../views/editor/pip-timer/PipRoot';
|
import PipRoot from '../../../views/editor/pip-timer/PipRoot';
|
||||||
|
|
||||||
import style from './MessageControl.module.scss';
|
import style from './TimerPreview.module.scss';
|
||||||
|
|
||||||
const secondarySourceLabels: Record<string, string> = {
|
const secondarySourceLabels: Record<string, string> = {
|
||||||
aux1: 'Aux 1',
|
aux1: 'Aux 1',
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.previewContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: $element-spacing;
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $element-spacing;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { setMessage, useTimerViewControl } from '../../../common/hooks/useSocket
|
|||||||
|
|
||||||
import TimerPreview from './TimerPreview';
|
import TimerPreview from './TimerPreview';
|
||||||
|
|
||||||
import style from './MessageControl.module.scss';
|
import style from './TimerViewControl.module.scss';
|
||||||
|
|
||||||
export default function TimerControlsPreview() {
|
export default function TimerControlsPreview() {
|
||||||
const { blackout, blink } = useTimerViewControl();
|
const { blackout, blink } = useTimerViewControl();
|
||||||
@@ -71,12 +71,13 @@ function SecondarySourceControl() {
|
|||||||
{ value: 'aux3', label: 'Aux 3' },
|
{ value: 'aux3', label: 'Aux 3' },
|
||||||
{ value: 'secondary', label: 'Secondary message' },
|
{ value: 'secondary', label: 'Secondary message' },
|
||||||
]}
|
]}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value: SecondarySource | null) => {
|
||||||
|
if (value === null) return;
|
||||||
// we can only update the remote if it is enabled
|
// we can only update the remote if it is enabled
|
||||||
if (secondarySource !== null) {
|
if (secondarySource !== null) {
|
||||||
setMessage.timerSecondarySource(value as SecondarySource);
|
setMessage.timerSecondarySource(value);
|
||||||
}
|
}
|
||||||
setValue(value as SecondarySource);
|
setValue(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.content {
|
||||||
|
padding-top: $panel-top-padding;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { CornerExtract } from '../../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||||
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
||||||
@@ -9,7 +9,7 @@ import { getIsNavigationLocked } from '../../../externals';
|
|||||||
|
|
||||||
import PlaybackControl from './PlaybackControl';
|
import PlaybackControl from './PlaybackControl';
|
||||||
|
|
||||||
import style from '../../../views/editor/Editor.module.scss';
|
import style from './TimerControlExport.module.scss';
|
||||||
|
|
||||||
export default memo(TimerControlExport);
|
export default memo(TimerControlExport);
|
||||||
function TimerControlExport() {
|
function TimerControlExport() {
|
||||||
@@ -17,8 +17,8 @@ function TimerControlExport() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectRoute permission='editor'>
|
<ProtectRoute permission='editor'>
|
||||||
<div className={style.playback} data-testid='panel-timer-control'>
|
<Editor.Panel data-testid='panel-timer-control'>
|
||||||
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('timercontrol', event)} />}
|
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('timercontrol', event)} />}
|
||||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||||
|
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
@@ -26,7 +26,7 @@ function TimerControlExport() {
|
|||||||
<PlaybackControl />
|
<PlaybackControl />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Editor.Panel>
|
||||||
</ProtectRoute>
|
</ProtectRoute>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,10 @@
|
|||||||
import { use, useCallback, useEffect, useMemo, useState } from 'react';
|
import { useWakelock } from './useWakeLock';
|
||||||
import { useSearchParams } from 'react-router';
|
|
||||||
|
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
|
||||||
|
|
||||||
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
|
|
||||||
export default function KeepAwake() {
|
|
||||||
const { keepAwake } = useKeepAwakeOptions();
|
|
||||||
const [wakeLockSentinel, setWakeLockSentinel] = useState<WakeLockSentinel | null>(null);
|
|
||||||
|
|
||||||
const removeLock = () => {
|
|
||||||
if (wakeLockSentinel) wakeLockSentinel.release().finally(() => setWakeLockSentinel(null));
|
|
||||||
};
|
|
||||||
|
|
||||||
const acquireLock = () => {
|
|
||||||
if (!wakeLockSentinel || wakeLockSentinel.released) {
|
|
||||||
setWakeLockSentinel(null);
|
|
||||||
navigator.wakeLock
|
|
||||||
.request('screen')
|
|
||||||
.then((sentinel) => {
|
|
||||||
setWakeLockSentinel(sentinel);
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
if (keepAwake) {
|
|
||||||
acquireLock();
|
|
||||||
document.addEventListener(
|
|
||||||
'visibilitychange',
|
|
||||||
() => {
|
|
||||||
if (wakeLockSentinel !== null && document.visibilityState === 'visible') {
|
|
||||||
acquireLock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ signal: controller.signal },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
removeLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
removeLock();
|
|
||||||
};
|
|
||||||
}, [keepAwake]);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keepAwakeKey = 'keep-awake';
|
|
||||||
|
|
||||||
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
|
|
||||||
// Helper to get value from either source, prioritizing defaultValues
|
|
||||||
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook exposes the keep awake options
|
* Composes the wakelock hook to contain re-renders
|
||||||
*/
|
*/
|
||||||
export function useKeepAwakeOptions() {
|
export default function KeepAwake() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
useWakelock();
|
||||||
const maybePreset = use(PresetContext);
|
|
||||||
|
|
||||||
const keepAwake = useMemo(() => {
|
return null;
|
||||||
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
|
|
||||||
return getOptionsFromParams(searchParams, defaultValues);
|
|
||||||
}, [maybePreset, searchParams]);
|
|
||||||
|
|
||||||
const toggleKeepAwake = useCallback(() => {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
if (keepAwake) {
|
|
||||||
searchParams.delete(keepAwakeKey);
|
|
||||||
} else {
|
|
||||||
searchParams.set(keepAwakeKey, '1');
|
|
||||||
}
|
|
||||||
return searchParams;
|
|
||||||
});
|
|
||||||
}, [keepAwake]);
|
|
||||||
|
|
||||||
return { keepAwake, toggleKeepAwake };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { use, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
|
|
||||||
|
// wakelock is only available in secure contexts
|
||||||
|
// however, that is covered by the navigator check
|
||||||
|
export const canUseWakeLock = typeof window !== 'undefined' && 'wakeLock' in navigator;
|
||||||
|
|
||||||
|
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
|
||||||
|
export function useWakelock() {
|
||||||
|
const { keepAwake } = useKeepAwakeOptions();
|
||||||
|
const wakeLockSentinelRef = useRef<WakeLockSentinel | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const removeLock = () => {
|
||||||
|
if (wakeLockSentinelRef.current) {
|
||||||
|
wakeLockSentinelRef.current.release().finally(() => {
|
||||||
|
wakeLockSentinelRef.current = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const acquireLock = () => {
|
||||||
|
const sentinel = wakeLockSentinelRef.current;
|
||||||
|
if (!sentinel || sentinel.released) {
|
||||||
|
wakeLockSentinelRef.current = null;
|
||||||
|
|
||||||
|
if (!canUseWakeLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.wakeLock
|
||||||
|
.request('screen')
|
||||||
|
.then((sentinel) => {
|
||||||
|
wakeLockSentinelRef.current = sentinel;
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
if (keepAwake) {
|
||||||
|
acquireLock();
|
||||||
|
document.addEventListener(
|
||||||
|
'visibilitychange',
|
||||||
|
() => {
|
||||||
|
if (wakeLockSentinelRef.current !== null && document.visibilityState === 'visible') {
|
||||||
|
acquireLock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal: controller.signal },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
removeLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
removeLock();
|
||||||
|
};
|
||||||
|
}, [keepAwake]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keepAwakeKey = 'keep-awake';
|
||||||
|
|
||||||
|
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
|
||||||
|
// Helper to get value from either source, prioritizing defaultValues
|
||||||
|
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook exposes the keep awake options
|
||||||
|
*/
|
||||||
|
export function useKeepAwakeOptions() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const maybePreset = use(PresetContext);
|
||||||
|
|
||||||
|
const keepAwake = useMemo(() => {
|
||||||
|
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
|
||||||
|
return getOptionsFromParams(searchParams, defaultValues);
|
||||||
|
}, [maybePreset, searchParams]);
|
||||||
|
|
||||||
|
const toggleKeepAwake = useCallback(() => {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
if (keepAwake) {
|
||||||
|
searchParams.delete(keepAwakeKey);
|
||||||
|
} else {
|
||||||
|
searchParams.set(keepAwakeKey, '1');
|
||||||
|
}
|
||||||
|
return searchParams;
|
||||||
|
});
|
||||||
|
}, [keepAwake, setSearchParams]);
|
||||||
|
|
||||||
|
return { keepAwake, toggleKeepAwake };
|
||||||
|
}
|
||||||
@@ -173,7 +173,7 @@ function Operator({ rundown, rundownMetadata, customFields, settings }: Operator
|
|||||||
if (isOntimeGroup(entry)) {
|
if (isOntimeGroup(entry)) {
|
||||||
const { isPast } = rundownMetadata[entry.id];
|
const { isPast } = rundownMetadata[entry.id];
|
||||||
|
|
||||||
const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId].groupId === entry.id : false;
|
const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId]?.groupId === entry.id : false;
|
||||||
|
|
||||||
if (hidePast && isPast && !isCurrentParent) {
|
if (hidePast && isPast && !isCurrentParent) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Fragment, useRef, useState } from 'react';
|
import { Fragment, useRef, useState } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { Dialog } from '@base-ui-components/react/dialog';
|
import { Dialog } from '@base-ui/react/dialog';
|
||||||
import { OntimeEvent } from 'ontime-types';
|
import { OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../../common/components/buttons/Button';
|
import Button from '../../../common/components/buttons/Button';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
@use '@/theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
@import '../Operator.module.scss';
|
|
||||||
|
|
||||||
.event {
|
.event {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import DelayIndicator from '../../../common/components/delay-indicator/DelayIndi
|
|||||||
import { useLongPress } from '../../../common/hooks/useLongPress';
|
import { useLongPress } from '../../../common/hooks/useLongPress';
|
||||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
import { formatDuration, formatTime, useTimeUntilExpectedStart } from '../../../common/utils/time';
|
import { formatDuration, formatTime, useTimeUntilExpectedStart } from '../../../common/utils/time';
|
||||||
import RunningTime from '../../viewers/common/running-time/RunningTime';
|
import RunningTime from '../../../views/common/running-time/RunningTime';
|
||||||
import SuperscriptPeriod from '../../viewers/common/superscript-time/SuperscriptPeriod';
|
import SuperscriptPeriod from '../../../views/common/superscript-time/SuperscriptPeriod';
|
||||||
import type { EditEvent, Subscribed } from '../operator.types';
|
import type { EditEvent, Subscribed } from '../operator.types';
|
||||||
|
|
||||||
import style from './OperatorEvent.module.scss';
|
import style from './OperatorEvent.module.scss';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
makeOptionsFromCustomFields,
|
makeOptionsFromCustomFields,
|
||||||
} from '../../common/components/view-params-editor/viewParams.utils';
|
} from '../../common/components/view-params-editor/viewParams.utils';
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import { isStringBoolean } from '../viewers/common/viewUtils';
|
import { isStringBoolean } from '../../views/common/viewUtils';
|
||||||
|
|
||||||
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
||||||
const fieldOptions = makeOptionsFromCustomFields(customFields, [
|
const fieldOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CustomFields, MaybeString, OntimeEvent } from 'ontime-types';
|
import { CustomFields, MaybeString, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { getPropertyValue } from '../viewers/common/viewUtils';
|
import { getPropertyValue } from '../../views/common/viewUtils';
|
||||||
|
|
||||||
import type { Subscribed } from './operator.types';
|
import type { Subscribed } from './operator.types';
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { useEntry } from '../../../common/hooks-query/useRundown';
|
|||||||
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
||||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import SuperscriptPeriod from '../../viewers/common/superscript-time/SuperscriptPeriod';
|
import SuperscriptPeriod from '../../../views/common/superscript-time/SuperscriptPeriod';
|
||||||
import { calculateEndAndDaySpan, formatDueTime } from '../overview.utils';
|
import { calculateEndAndDaySpan, formatDueTime } from '../overview.utils';
|
||||||
|
|
||||||
import { OverUnder, TimeColumn, WrappedInTimeColumn } from './TimeLayout';
|
import { OverUnder, TimeColumn, WrappedInTimeColumn } from './TimeLayout';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { TbFlagFilled } from 'react-icons/tb';
|
import { TbFlagFilled } from 'react-icons/tb';
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -36,7 +36,6 @@ import { useEntryActions } from '../../common/hooks/useEntryAction';
|
|||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||||
import { cloneEvent } from '../../common/utils/clone';
|
|
||||||
import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
||||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||||
|
|
||||||
@@ -58,6 +57,8 @@ interface RundownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||||
|
'use memo';
|
||||||
|
|
||||||
const { order, entries, id } = data;
|
const { order, entries, id } = data;
|
||||||
// we create a copy of the rundown with a data structured aligned with what dnd-kit needs
|
// we create a copy of the rundown with a data structured aligned with what dnd-kit needs
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
@@ -68,17 +69,20 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
key: `rundown.${id}-editor-collapsed-groups`,
|
key: `rundown.${id}-editor-collapsed-groups`,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
});
|
});
|
||||||
|
const collapsedGroupSet = useMemo(() => new Set(collapsedGroups), [collapsedGroups]);
|
||||||
|
|
||||||
const { addEntry, deleteEntry, move, reorderEntry } = useEntryActions();
|
const { addEntry, clone, deleteEntry, move, reorderEntry } = useEntryActions();
|
||||||
|
const setEntryCopyId = useEntryCopy((state) => state.setEntryCopyId);
|
||||||
const { entryCopyId, setEntryCopyId } = useEntryCopy();
|
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
const [editorMode] = useSessionStorage<AppMode>({
|
const [editorMode] = useSessionStorage<AppMode>({
|
||||||
key: sessionKeys.editorMode,
|
key: sessionKeys.editorMode,
|
||||||
defaultValue: AppMode.Edit,
|
defaultValue: AppMode.Edit,
|
||||||
});
|
});
|
||||||
const { clearSelectedEvents, setSelectedEvents, cursor } = useEventSelection();
|
|
||||||
|
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
|
||||||
|
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
|
||||||
|
const cursor = useEventSelection((state) => state.cursor);
|
||||||
|
|
||||||
const cursorRef = useRef<HTMLDivElement | null>(null);
|
const cursorRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -105,20 +109,30 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const insertCopyAtId = useCallback(
|
const insertCopyAtId = useCallback(
|
||||||
(atId: string | null, copyId: string | null, above = false) => {
|
(atId: string | null, above = false) => {
|
||||||
const adjustedCursor = above ? getPreviousNormal(entries, order, atId ?? '').entry?.id ?? null : atId;
|
// lazily get the value from the store
|
||||||
if (copyId === null) {
|
const { entryCopyId } = useEntryCopy.getState();
|
||||||
|
if (entryCopyId === null || !entries[entryCopyId]) {
|
||||||
// we cant clone without selection
|
// we cant clone without selection
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cloneEntry = entries[copyId];
|
|
||||||
if (cloneEntry?.type === SupportedEntry.Event) {
|
let normalisedAtId = atId;
|
||||||
//if we don't have a cursor add the new event on top
|
|
||||||
const newEvent = cloneEvent(cloneEntry);
|
const elementToCopy = entries[entryCopyId];
|
||||||
addEntry(newEvent, { after: adjustedCursor ?? undefined });
|
const refElement = atId ? entries[atId] : undefined;
|
||||||
|
|
||||||
|
if (refElement && 'parent' in refElement && refElement.parent && elementToCopy.type === SupportedEntry.Group) {
|
||||||
|
normalisedAtId = refElement.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(entryCopyId, {
|
||||||
|
after: above ? undefined : normalisedAtId ?? undefined,
|
||||||
|
// if we don't have a cursor add the new event on top
|
||||||
|
before: above ? normalisedAtId ?? undefined : undefined,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[addEntry, order, entries],
|
[entries, clone],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -200,9 +214,9 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
*/
|
*/
|
||||||
const getIsCollapsed = useCallback(
|
const getIsCollapsed = useCallback(
|
||||||
(groupId: EntryId): boolean => {
|
(groupId: EntryId): boolean => {
|
||||||
return Boolean(collapsedGroups.find((id) => id === groupId));
|
return collapsedGroupSet.has(groupId);
|
||||||
},
|
},
|
||||||
[collapsedGroups],
|
[collapsedGroupSet],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,12 +314,8 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
],
|
],
|
||||||
|
|
||||||
['mod + C', () => setEntryCopyId(cursor)],
|
['mod + C', () => setEntryCopyId(cursor)],
|
||||||
['mod + V', () => insertCopyAtId(cursor, entryCopyId)],
|
['mod + V', () => insertCopyAtId(cursor)],
|
||||||
[
|
['mod + shift + V', () => insertCopyAtId(cursor, true), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
'mod + shift + V',
|
|
||||||
() => insertCopyAtId(cursor, entryCopyId, true),
|
|
||||||
{ preventDefault: true, usePhysicalKeys: true },
|
|
||||||
],
|
|
||||||
|
|
||||||
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||||
]);
|
]);
|
||||||
@@ -395,7 +405,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// keep copy of the current state in case we need to revert
|
// keep copy of the current state in case we need to revert
|
||||||
const currentEntries = structuredClone(sortableData);
|
const currentEntries = [...sortableData];
|
||||||
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
||||||
setSortableData((currentEntries) => {
|
setSortableData((currentEntries) => {
|
||||||
return reorderArray(currentEntries, fromIndex, toIndex);
|
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||||
@@ -438,9 +448,12 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
return <RundownEmpty handleAddNew={(type: SupportedEntry) => addEntry({ type })} />;
|
return <RundownEmpty handleAddNew={(type: SupportedEntry) => addEntry({ type })} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. gather presentation options
|
// gather presentation options
|
||||||
const isEditMode = editorMode === AppMode.Edit;
|
const isEditMode = editorMode === AppMode.Edit;
|
||||||
|
|
||||||
|
// gather rundown wide data
|
||||||
|
const lastEntryId = order.at(-1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -509,7 +522,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
|||||||
const groupColour = entryMetadata.groupColour === '' ? '#9d9d9d' : entryMetadata.groupColour;
|
const groupColour = entryMetadata.groupColour === '' ? '#9d9d9d' : entryMetadata.groupColour;
|
||||||
|
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = entryId === order.at(-1);
|
const isLast = entryId === lastEntryId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We need to provide the parent ID for the QuickAdd components
|
* We need to provide the parent ID for the QuickAdd components
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
import {
|
import { isOntimeDelay, isOntimeEvent, isOntimeMilestone, OntimeEntry, Playback, SupportedEntry } from 'ontime-types';
|
||||||
isOntimeDelay,
|
|
||||||
isOntimeEvent,
|
|
||||||
isOntimeMilestone,
|
|
||||||
OntimeEntry,
|
|
||||||
OntimeEvent,
|
|
||||||
Playback,
|
|
||||||
SupportedEntry,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
|
||||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
|
||||||
import { cloneEvent } from '../../common/utils/clone';
|
|
||||||
|
|
||||||
import RundownDelay from './rundown-delay/RundownDelay';
|
import RundownDelay from './rundown-delay/RundownDelay';
|
||||||
import RundownEvent from './rundown-event/RundownEvent';
|
import RundownEvent from './rundown-event/RundownEvent';
|
||||||
@@ -44,13 +32,6 @@ export default function RundownEntry({
|
|||||||
totalGap,
|
totalGap,
|
||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
}: RundownEntryProps) {
|
}: RundownEntryProps) {
|
||||||
const { addEntry } = useEntryActions();
|
|
||||||
|
|
||||||
const createCloneEvent = useMemoisedFn(() => {
|
|
||||||
const newEvent = cloneEvent(data as OntimeEvent);
|
|
||||||
addEntry(newEvent, { after: data.id });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isOntimeEvent(data)) {
|
if (isOntimeEvent(data)) {
|
||||||
return (
|
return (
|
||||||
<RundownEvent
|
<RundownEvent
|
||||||
@@ -83,7 +64,6 @@ export default function RundownEntry({
|
|||||||
dayOffset={data.dayOffset}
|
dayOffset={data.dayOffset}
|
||||||
totalGap={totalGap}
|
totalGap={totalGap}
|
||||||
isLinkedToLoaded={isLinkedToLoaded}
|
isLinkedToLoaded={isLinkedToLoaded}
|
||||||
createCloneEvent={createCloneEvent}
|
|
||||||
hasTriggers={data.triggers.length > 0}
|
hasTriggers={data.triggers.length > 0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@use '../../views/editor/EditorMixin' as editor;
|
|
||||||
|
|
||||||
.rundownExport {
|
.rundownExport {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
|
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
|
||||||
@@ -23,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@include editor.panel;
|
display: flex;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
box-shadow: $box-shadow-right;
|
box-shadow: $box-shadow-right;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
|
|
||||||
import { CornerExtract } from '../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../common/components/editor-utils/EditorUtils';
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||||
@@ -58,14 +58,14 @@ function RundownExport() {
|
|||||||
<FinderPlacement />
|
<FinderPlacement />
|
||||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||||
<div className={style.rundown}>
|
<div className={style.rundown}>
|
||||||
<div className={style.list}>
|
<Editor.Panel className={style.list}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||||
<RundownContextMenu>
|
<RundownContextMenu>
|
||||||
<RundownWrapper />
|
<RundownWrapper />
|
||||||
</RundownContextMenu>
|
</RundownContextMenu>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</Editor.Panel>
|
||||||
{!hideSideBar && (
|
{!hideSideBar && (
|
||||||
<div className={style.side}>
|
<div className={style.side}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
|||||||
@@ -102,7 +102,10 @@ function EventEditorTimes({
|
|||||||
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
|
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
|
||||||
<Select
|
<Select
|
||||||
value={endAction}
|
value={endAction}
|
||||||
onValueChange={(value) => handleSubmit('endAction', value)}
|
onValueChange={(value: EndAction | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
handleSubmit('endAction', value);
|
||||||
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: EndAction.None, label: 'None' },
|
{ value: EndAction.None, label: 'None' },
|
||||||
{ value: EndAction.LoadNext, label: 'Load next event' },
|
{ value: EndAction.LoadNext, label: 'Load next event' },
|
||||||
@@ -139,7 +142,10 @@ function EventEditorTimes({
|
|||||||
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
|
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
|
||||||
<Select
|
<Select
|
||||||
value={timerType}
|
value={timerType}
|
||||||
onValueChange={(value) => handleSubmit('timerType', value)}
|
onValueChange={(value: TimerType | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
handleSubmit('timerType', value);
|
||||||
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: TimerType.CountDown, label: 'Count down' },
|
{ value: TimerType.CountDown, label: 'Count down' },
|
||||||
{ value: TimerType.CountUp, label: 'Count up' },
|
{ value: TimerType.CountUp, label: 'Count up' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
import { MaybeString, SupportedEntry } from 'ontime-types';
|
import { MaybeString, SupportedEntry } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ interface RundownEventProps {
|
|||||||
dayOffset: number;
|
dayOffset: number;
|
||||||
totalGap: number;
|
totalGap: number;
|
||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
createCloneEvent: () => void;
|
|
||||||
hasTriggers: boolean;
|
hasTriggers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +90,9 @@ export default function RundownEvent({
|
|||||||
totalGap,
|
totalGap,
|
||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
hasTriggers,
|
hasTriggers,
|
||||||
createCloneEvent,
|
|
||||||
}: RundownEventProps) {
|
}: RundownEventProps) {
|
||||||
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||||
const { updateEntry, batchUpdateEvents, deleteEntry, groupEntries, swapEvents } = useEntryActions();
|
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActions();
|
||||||
|
|
||||||
const { selectedEvents, unselect, setSelectedEvents, clearSelectedEvents } = useEventSelection();
|
const { selectedEvents, unselect, setSelectedEvents, clearSelectedEvents } = useEventSelection();
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
@@ -172,7 +170,7 @@ export default function RundownEvent({
|
|||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Clone',
|
label: 'Clone',
|
||||||
icon: IoDuplicateOutline,
|
icon: IoDuplicateOutline,
|
||||||
onClick: createCloneEvent,
|
onClick: () => clone(eventId, { after: eventId }),
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Toggle } from '@base-ui-components/react/toggle';
|
import { Toggle } from '@base-ui/react/toggle';
|
||||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
import { OffsetMode } from 'ontime-types';
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Toggle } from '@base-ui-components/react/toggle';
|
import { Toggle } from '@base-ui/react/toggle';
|
||||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
import { OffsetMode } from 'ontime-types';
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { IoTrash } from 'react-icons/io5';
|
import { IoTrash } from 'react-icons/io5';
|
||||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
import { useDisclosure, useSessionStorage } from '@mantine/hooks';
|
import { useDisclosure, useSessionStorage } from '@mantine/hooks';
|
||||||
|
|
||||||
import Button from '../../../common/components/buttons/Button';
|
import Button from '../../../common/components/buttons/Button';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Input from '../../common/components/input/input/Input';
|
|||||||
import Select from '../../common/components/select/Select';
|
import Select from '../../common/components/select/Select';
|
||||||
import Switch from '../../common/components/switch/Switch';
|
import Switch from '../../common/components/switch/Switch';
|
||||||
import { useUpdateUrlPreset } from '../../common/hooks-query/useUrlPresets';
|
import { useUpdateUrlPreset } from '../../common/hooks-query/useUrlPresets';
|
||||||
import copyToClipboard from '../../common/utils/copyToClipboard';
|
import { safeCopyToClipboard } from '../../common/utils/copyToClipboard';
|
||||||
import { preventEscape } from '../../common/utils/keyEvent';
|
import { preventEscape } from '../../common/utils/keyEvent';
|
||||||
import { isUrlSafe } from '../../common/utils/regex';
|
import { isUrlSafe } from '../../common/utils/regex';
|
||||||
import { isOntimeCloud, serverURL } from '../../externals';
|
import { isOntimeCloud, serverURL } from '../../externals';
|
||||||
@@ -125,7 +125,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
|||||||
lockNav: options.lockNav,
|
lockNav: options.lockNav,
|
||||||
preset: urlPreset.alias,
|
preset: urlPreset.alias,
|
||||||
});
|
});
|
||||||
await copyToClipboard(url);
|
await safeCopyToClipboard(url);
|
||||||
setUrl(url);
|
setUrl(url);
|
||||||
} else {
|
} else {
|
||||||
const presetPath = options.path.startsWith('preset-') ? options.path.replace('preset-', '') : undefined;
|
const presetPath = options.path.startsWith('preset-') ? options.path.replace('preset-', '') : undefined;
|
||||||
@@ -143,7 +143,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
|||||||
preset: presetPath,
|
preset: presetPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await copyToClipboard(url);
|
await safeCopyToClipboard(url);
|
||||||
setUrl(url);
|
setUrl(url);
|
||||||
}
|
}
|
||||||
reset(options, {
|
reset(options, {
|
||||||
@@ -179,7 +179,10 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
|||||||
<Select
|
<Select
|
||||||
options={hostOptions}
|
options={hostOptions}
|
||||||
value={watch('baseUrl')}
|
value={watch('baseUrl')}
|
||||||
onValueChange={(value) => setValue('baseUrl', value)}
|
onValueChange={(value: string | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('baseUrl', value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
)}
|
)}
|
||||||
@@ -191,7 +194,10 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
|||||||
<Select
|
<Select
|
||||||
options={pathOptions}
|
options={pathOptions}
|
||||||
value={watch('path')}
|
value={watch('path')}
|
||||||
onValueChange={(value) => setValue('path', value, { shouldDirty: true })}
|
onValueChange={(value: OntimeView | string | null) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setValue('path', value, { shouldDirty: true });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ $component-border-radius-md: 3px;
|
|||||||
$component-border-radius-sm: 2px;
|
$component-border-radius-sm: 2px;
|
||||||
$component-border-radius-full: 99px;
|
$component-border-radius-full: 99px;
|
||||||
|
|
||||||
|
$panel-border-radius: 8px;
|
||||||
|
$panel-gap: 0.5rem;
|
||||||
|
$panel-top-padding: 1.5rem;
|
||||||
|
|
||||||
// indexes
|
// indexes
|
||||||
$zindex-floating: 100;
|
$zindex-floating: 100;
|
||||||
$zindex-nav: 200;
|
$zindex-nav: 200;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@forward 'ontimeColours';
|
||||||
|
@forward 'ontimeStyles';
|
||||||
|
@forward 'mixins';
|
||||||
@@ -14,10 +14,10 @@ import { useBackstageSocket, useClock } from '../../common/hooks/useSocket';
|
|||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../common/loader/Loader';
|
import Loader from '../common/loader/Loader';
|
||||||
import ScheduleExport from '../common/schedule/ScheduleExport';
|
import ScheduleExport from '../common/schedule/ScheduleExport';
|
||||||
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import { getBackstageOptions, useBackstageOptions } from './backstage.options';
|
import { getBackstageOptions, useBackstageOptions } from './backstage.options';
|
||||||
import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils';
|
import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MaybeNumber, OntimeEvent, Playback, TimerPhase } from 'ontime-types';
|
import { MaybeNumber, OntimeEvent, Playback, TimerPhase } from 'ontime-types';
|
||||||
|
|
||||||
import { enDash } from '../../common/utils/styleUtils';
|
import { enDash } from '../../common/utils/styleUtils';
|
||||||
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
|
import { getPropertyValue } from '../common/viewUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the current time is in overtime
|
* Whether the current time is in overtime
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
|
||||||
import { formatTime } from '../../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { FORMAT_12, FORMAT_24 } from '../../../../viewerConfig';
|
import { FORMAT_12, FORMAT_24 } from '../../../viewerConfig';
|
||||||
import SuperscriptTime from '../superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
interface ClockTimeProps {
|
interface ClockTimeProps {
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
import { removeLeadingZero, removeSeconds } from 'ontime-utils';
|
import { removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { formattedTime } from '../../../overview/overview.utils';
|
import { formattedTime } from '../../../features/overview/overview.utils';
|
||||||
|
|
||||||
interface RunningTimeProps {
|
interface RunningTimeProps {
|
||||||
value: MaybeNumber;
|
value: MaybeNumber;
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
import { createContext, PropsWithChildren, RefObject, use, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import {
|
||||||
|
createContext,
|
||||||
|
PropsWithChildren,
|
||||||
|
RefObject,
|
||||||
|
use,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { EntryId, isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
import { EntryId, isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
||||||
@@ -22,14 +32,20 @@ interface ScheduleProviderProps {
|
|||||||
|
|
||||||
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
|
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
|
||||||
const { cycleInterval, stopCycle, filter } = useScheduleOptions();
|
const { cycleInterval, stopCycle, filter } = useScheduleOptions();
|
||||||
const { data: events } = usePartialRundown((entry: ExtendedEntry<OntimeEntry>) => {
|
|
||||||
if (filter) {
|
const filterCallback = useCallback(
|
||||||
// custom keys are prepended with custom-
|
(entry: ExtendedEntry<OntimeEntry>) => {
|
||||||
const customKey = filter.startsWith('custom-') ? filter.slice('custom-'.length) : filter;
|
if (filter) {
|
||||||
return isOntimeEvent(entry) && Boolean(entry.custom[customKey]);
|
// custom keys are prepended with custom-
|
||||||
}
|
const customKey = filter.startsWith('custom-') ? filter.slice('custom-'.length) : filter;
|
||||||
return isOntimeEvent(entry);
|
return isOntimeEvent(entry) && Boolean(entry.custom[customKey]);
|
||||||
});
|
}
|
||||||
|
return isOntimeEvent(entry);
|
||||||
|
},
|
||||||
|
[filter],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: events } = usePartialRundown(filterCallback);
|
||||||
|
|
||||||
const [firstIndex, setFirstIndex] = useState(-1);
|
const [firstIndex, setFirstIndex] = useState(-1);
|
||||||
const [numPages, setNumPages] = useState(0);
|
const [numPages, setNumPages] = useState(0);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getOffsetState } from '../../../common/utils/offset';
|
|||||||
import { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
import { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime, getExpectedTimesFromExtendedEvent } from '../../../common/utils/time';
|
import { formatTime, getExpectedTimesFromExtendedEvent } from '../../../common/utils/time';
|
||||||
import SuperscriptPeriod from '../../../features/viewers/common/superscript-time/SuperscriptPeriod';
|
import SuperscriptPeriod from '../superscript-time/SuperscriptPeriod';
|
||||||
|
|
||||||
import { useScheduleOptions } from './schedule.options';
|
import { useScheduleOptions } from './schedule.options';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router';
|
|||||||
import { SelectOption } from '../../../common/components/select/Select';
|
import { SelectOption } from '../../../common/components/select/Select';
|
||||||
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||||
import type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
import type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../viewUtils';
|
||||||
|
|
||||||
export const getScheduleOptions = (customFieldOptions: SelectOption[]): ViewOption => ({
|
export const getScheduleOptions = (customFieldOptions: SelectOption[]): ViewOption => ({
|
||||||
title: OptionTitle.Schedule,
|
title: OptionTitle.Schedule,
|
||||||
|
|||||||
+3
-11
@@ -1,8 +1,8 @@
|
|||||||
import { MaybeNumber, MaybeString, OntimeEvent, TimerState, TimerType } from 'ontime-types';
|
import { MaybeNumber, MaybeString, OntimeEvent, TimerState, TimerType } from 'ontime-types';
|
||||||
import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
|
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../common/utils/time';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gathers all options that affect which timer is displayed and selects the correct data source to display
|
* Gathers all options that affect which timer is displayed and selects the correct data source to display
|
||||||
@@ -11,7 +11,6 @@ import { formatTime } from '../../../common/utils/time';
|
|||||||
export function getTimerByType(
|
export function getTimerByType(
|
||||||
freezeEnd: boolean,
|
freezeEnd: boolean,
|
||||||
timerTypeNow: TimerType,
|
timerTypeNow: TimerType,
|
||||||
countToEndNow: boolean,
|
|
||||||
clock: number,
|
clock: number,
|
||||||
timerObject: Pick<TimerState, 'current' | 'elapsed'>,
|
timerObject: Pick<TimerState, 'current' | 'elapsed'>,
|
||||||
timerTypeOverride?: TimerType,
|
timerTypeOverride?: TimerType,
|
||||||
@@ -22,13 +21,6 @@ export function getTimerByType(
|
|||||||
|
|
||||||
const viewTimerType = timerTypeOverride ?? timerTypeNow;
|
const viewTimerType = timerTypeOverride ?? timerTypeNow;
|
||||||
|
|
||||||
if (countToEndNow) {
|
|
||||||
if (timerObject.current === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (viewTimerType) {
|
switch (viewTimerType) {
|
||||||
case TimerType.CountDown:
|
case TimerType.CountDown:
|
||||||
if (timerObject.current === null) {
|
if (timerObject.current === null) {
|
||||||
@@ -36,7 +28,7 @@ export function getTimerByType(
|
|||||||
}
|
}
|
||||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
||||||
case TimerType.CountUp:
|
case TimerType.CountUp:
|
||||||
return Math.abs(timerObject.elapsed ?? 0);
|
return timerObject.elapsed;
|
||||||
case TimerType.Clock:
|
case TimerType.Clock:
|
||||||
return clock;
|
return clock;
|
||||||
case TimerType.None:
|
case TimerType.None:
|
||||||
@@ -11,9 +11,9 @@ import { useClock } from '../../common/hooks/useSocket';
|
|||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../common/loader/Loader';
|
import Loader from '../common/loader/Loader';
|
||||||
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
||||||
import { getOrderedSubscriptions } from './countdown.utils';
|
import { getOrderedSubscriptions } from './countdown.utils';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { EntryId, PlayableEvent } from 'ontime-types';
|
|||||||
import Button from '../../common/components/buttons/Button';
|
import Button from '../../common/components/buttons/Button';
|
||||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
|
import ClockTime from '../common/clock-time/ClockTime';
|
||||||
|
|
||||||
import { makeSubscriptionsUrl } from './countdown.utils';
|
import { makeSubscriptionsUrl } from './countdown.utils';
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
|||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { throttle } from '../../common/utils/throttle';
|
import { throttle } from '../../common/utils/throttle';
|
||||||
import FollowButton from '../../features/operator/follow-button/FollowButton';
|
import FollowButton from '../../features/operator/follow-button/FollowButton';
|
||||||
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
|
import ClockTime from '../common/clock-time/ClockTime';
|
||||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
|
import { getPropertyValue } from '../common/viewUtils';
|
||||||
|
|
||||||
import { useCountdownOptions } from './countdown.options';
|
import { useCountdownOptions } from './countdown.options';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { useExpectedStartData } from '../../common/hooks/useSocket';
|
|||||||
import useReport from '../../common/hooks-query/useReport';
|
import useReport from '../../common/hooks-query/useReport';
|
||||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
|
import { getPropertyValue } from '../common/viewUtils';
|
||||||
|
|
||||||
import { useCountdownOptions } from './countdown.options';
|
import { useCountdownOptions } from './countdown.options';
|
||||||
import { useSubscriptionDisplayData } from './countdown.utils';
|
import { useSubscriptionDisplayData } from './countdown.utils';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { OptionTitle } from '../../common/components/view-params-editor/constant
|
|||||||
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../common/viewUtils';
|
||||||
|
|
||||||
export const getCountdownOptions = (
|
export const getCountdownOptions = (
|
||||||
timeFormat: string,
|
timeFormat: string,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
@use './cuesheetRows' as rows;
|
||||||
|
|
||||||
$table-font-size: 1rem;
|
$table-font-size: 1rem;
|
||||||
$table-header-font-size: calc(1rem - 2px);
|
$table-header-font-size: calc(1rem - 2px);
|
||||||
|
|
||||||
|
@include rows.cuesheet-row-columns($table-header-font-size);
|
||||||
|
|
||||||
.cuesheet {
|
.cuesheet {
|
||||||
font-size: $table-font-size;
|
font-size: $table-font-size;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -49,27 +53,6 @@ $table-header-font-size: calc(1rem - 2px);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionColumn {
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
padding-top: 0.5rem !important; // fighting the table styles
|
|
||||||
background-color: transparent;
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indexColumn {
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
justify-content: end;
|
|
||||||
|
|
||||||
min-width: 3em; // allow for 3-digit numbers
|
|
||||||
font-size: $table-header-font-size;
|
|
||||||
line-height: 2rem; // match input height
|
|
||||||
background-color: $gray-1300; // will be overridden inline
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableHeader {
|
.tableHeader {
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@use '@/theme/themeTokens' as *;
|
||||||
|
|
||||||
|
@mixin cuesheet-row-columns($header-font-size: calc(1rem - 2px)) {
|
||||||
|
.actionColumn {
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
padding-top: 0.5rem !important; // fighting the table styles
|
||||||
|
background-color: transparent;
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indexColumn {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
min-width: 3em; // allow for 3-digit numbers
|
||||||
|
font-size: $header-font-size;
|
||||||
|
line-height: 2rem; // match input height
|
||||||
|
background-color: $gray-1300; // will be overridden inline
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
@import '../CuesheetTable.module.scss';
|
@use '../cuesheetRows' as rows;
|
||||||
|
@include rows.cuesheet-row-columns;
|
||||||
|
|
||||||
.delayRow {
|
.delayRow {
|
||||||
color: $ontime-delay-text;
|
color: $ontime-delay-text;
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
@import "../CuesheetTable.module.scss";
|
@use '../cuesheetRows' as rows;
|
||||||
|
@include rows.cuesheet-row-columns;
|
||||||
|
|
||||||
.eventRow {
|
.eventRow {
|
||||||
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
|
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
@import '../CuesheetTable.module.scss';
|
@use '../cuesheetRows' as rows;
|
||||||
|
@include rows.cuesheet-row-columns;
|
||||||
|
|
||||||
.groupRow {
|
.groupRow {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
@import "../CuesheetTable.module.scss";
|
@use '../cuesheetRows' as rows;
|
||||||
|
@include rows.cuesheet-row-columns;
|
||||||
|
|
||||||
.milestoneRow {
|
.milestoneRow {
|
||||||
background: color-mix(in srgb, transparent 98%, var(--user-bg, $gray-500) 2%);
|
background: color-mix(in srgb, transparent 98%, var(--user-bg, $gray-500) 2%);
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { OntimeView } from 'ontime-types';
|
import { OntimeView } from 'ontime-types';
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
import { ReactNode, use } from 'react';
|
import { ReactNode, use } from 'react';
|
||||||
import { IoChevronDown, IoOptions, IoSettingsOutline } from 'react-icons/io5';
|
import { IoChevronDown, IoOptions, IoSettingsOutline } from 'react-icons/io5';
|
||||||
import { Popover } from '@base-ui-components/react/popover';
|
import { Popover } from '@base-ui/react/popover';
|
||||||
import { Toggle } from '@base-ui-components/react/toggle';
|
import { Toggle } from '@base-ui/react/toggle';
|
||||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
import type { Column } from '@tanstack/react-table';
|
import type { Column } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,5 @@
|
|||||||
@use './EditorMixin' as editor;
|
|
||||||
|
|
||||||
$min-playback-width: 27rem;
|
$min-playback-width: 27rem;
|
||||||
$max-playback-width: 30rem;
|
$max-playback-width: 30rem;
|
||||||
$panel-gap: 0.5rem;
|
|
||||||
|
|
||||||
.mainContainer {
|
|
||||||
background-color: $ui-black;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
color: $ui-white;
|
|
||||||
padding: 0.5rem;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto;
|
|
||||||
grid-template-rows: 3.5rem 1fr;
|
|
||||||
grid-template-areas:
|
|
||||||
'overview'
|
|
||||||
'main';
|
|
||||||
gap: $panel-gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelContainer {
|
.panelContainer {
|
||||||
grid-area: main;
|
grid-area: main;
|
||||||
@@ -26,15 +7,6 @@ $panel-gap: 0.5rem;
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: $panel-gap;
|
gap: $panel-gap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.rundown,
|
|
||||||
.playback,
|
|
||||||
.messages {
|
|
||||||
position: relative;
|
|
||||||
border-radius: var(--editor--panel__br);
|
|
||||||
background-color: $bg-container-l2;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
@@ -45,18 +17,3 @@ $panel-gap: 0.5rem;
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $panel-gap;
|
gap: $panel-gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentColumnLayout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $section-spacing;
|
|
||||||
color: $ui-white;
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user