mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 22:24:11 +00:00
V3 feedback (#866)
* refactor: rename settings panel * style: typo * style: fix blackout position * fix: offset time in time-to-end * refactor: operator is protected * style: tweaks to pincode * refactor: close params editor on submit * refactor: same navigation in all pages * refactor: use cached response
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-useless-concat": "warn",
|
"no-useless-concat": "warn",
|
||||||
"prefer-template": "warn",
|
"prefer-template": "warn",
|
||||||
|
"no-throw-literal": "error",
|
||||||
"no-console": [
|
"no-console": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ $button-size: 3rem;
|
|||||||
|
|
||||||
.link {
|
.link {
|
||||||
@include action-link;
|
@include action-link;
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $menu-hover-bg;
|
background-color: $menu-hover-bg;
|
||||||
@@ -69,7 +69,7 @@ $button-size: 3rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.linkIcon {
|
.linkIcon {
|
||||||
display: inline-block;
|
margin-left: auto;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,43 @@
|
|||||||
import { memo, PropsWithChildren, useRef } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay } from '@chakra-ui/react';
|
import { Link } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerBody,
|
||||||
|
DrawerCloseButton,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useFullscreen } from '@mantine/hooks';
|
||||||
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
|
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||||
|
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||||
|
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
|
||||||
|
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||||
|
|
||||||
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import useClickOutside from '../../hooks/useClickOutside';
|
import useClickOutside from '../../hooks/useClickOutside';
|
||||||
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
|
import { isKeyEnter } from '../../utils/keyEvent';
|
||||||
|
|
||||||
|
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
||||||
|
|
||||||
|
import style from './NavigationMenu.module.scss';
|
||||||
|
|
||||||
interface NavigationMenuProps {
|
interface NavigationMenuProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenu(props: PropsWithChildren<NavigationMenuProps>) {
|
function NavigationMenu(props: NavigationMenuProps) {
|
||||||
const { children, isOpen, onClose } = props;
|
const { isOpen, onClose } = props;
|
||||||
|
|
||||||
|
const { isOpen: isRenameOpen, onOpen: onRenameOpen, onClose: onRenameClose } = useDisclosure();
|
||||||
|
|
||||||
|
const { fullscreen, toggle } = useFullscreen();
|
||||||
|
const { toggleMirror } = useViewOptionsStore();
|
||||||
|
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -18,6 +45,7 @@ function NavigationMenu(props: PropsWithChildren<NavigationMenuProps>) {
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div id='navigation-menu-portal' ref={menuRef}>
|
<div id='navigation-menu-portal' ref={menuRef}>
|
||||||
|
<RenameClientModal isOpen={isRenameOpen} onClose={onRenameClose} />
|
||||||
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
@@ -25,7 +53,81 @@ function NavigationMenu(props: PropsWithChildren<NavigationMenuProps>) {
|
|||||||
<DrawerCloseButton size='lg' />
|
<DrawerCloseButton size='lg' />
|
||||||
Ontime
|
Ontime
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerBody padding={0}>{children}</DrawerBody>
|
<DrawerBody padding={0}>
|
||||||
|
<div className={style.buttonsContainer}>
|
||||||
|
<div
|
||||||
|
className={style.link}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={toggle}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && toggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Toggle Fullscreen
|
||||||
|
{fullscreen ? <IoContract /> : <IoExpand />}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={style.link}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={() => toggleMirror()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && toggleMirror();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Flip Screen
|
||||||
|
<IoSwapVertical />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={style.link}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={onRenameOpen}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && onRenameOpen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Rename Client
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className={style.separator} />
|
||||||
|
<Link
|
||||||
|
to='/editor'
|
||||||
|
className={`${style.link} ${location.pathname === '/editor' ? style.current : ''}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
Editor
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to='/cuesheet'
|
||||||
|
className={`${style.link} ${location.pathname === '/cuesheet' ? style.current : ''}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
Cuesheet
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</Link>
|
||||||
|
<Link to='/op' className={`${style.link} ${location.pathname === '/op' ? style.current : ''}`} tabIndex={0}>
|
||||||
|
<IoLockClosedOutline />
|
||||||
|
Operator
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</Link>
|
||||||
|
<hr className={style.separator} />
|
||||||
|
{navigatorConstants.map((route) => (
|
||||||
|
<Link
|
||||||
|
key={route.url}
|
||||||
|
to={route.url}
|
||||||
|
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{route.label}
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</DrawerBody>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { useFullscreen } from '@mantine/hooks';
|
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|
||||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
|
||||||
import { isKeyEnter } from '../../utils/keyEvent';
|
|
||||||
|
|
||||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
|
||||||
import NavigationMenu from './NavigationMenu';
|
import NavigationMenu from './NavigationMenu';
|
||||||
|
|
||||||
import style from './NavigationMenu.module.scss';
|
|
||||||
|
|
||||||
interface ProductionNavigationMenuProps {
|
interface ProductionNavigationMenuProps {
|
||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
onMenuClose: () => void;
|
onMenuClose: () => void;
|
||||||
@@ -21,73 +9,8 @@ interface ProductionNavigationMenuProps {
|
|||||||
|
|
||||||
function ProductionNavigationMenu(props: ProductionNavigationMenuProps) {
|
function ProductionNavigationMenu(props: ProductionNavigationMenuProps) {
|
||||||
const { isMenuOpen, onMenuClose } = props;
|
const { isMenuOpen, onMenuClose } = props;
|
||||||
const location = useLocation();
|
|
||||||
const { fullscreen, toggle } = useFullscreen();
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
return <NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />;
|
||||||
<NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose}>
|
|
||||||
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
|
||||||
<div className={style.buttonsContainer}>
|
|
||||||
<div
|
|
||||||
className={style.link}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={toggle}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && toggle();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Toggle Fullscreen
|
|
||||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={style.link}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={onOpen}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && onOpen();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Rename Client
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className={style.separator} />
|
|
||||||
<Link
|
|
||||||
to='/editor'
|
|
||||||
className={`${style.link} ${location.pathname === '/editor' ? style.current : ''}`}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Editor
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to='/cuesheet'
|
|
||||||
className={`${style.link} ${location.pathname === '/cuesheet' ? style.current : ''}`}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Cuesheet
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
<Link to='/op' className={`${style.link} ${location.pathname === '/op' ? style.current : ''}`} tabIndex={0}>
|
|
||||||
Operator
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
<hr className={style.separator} />
|
|
||||||
{navigatorConstants.map((route) => (
|
|
||||||
<Link
|
|
||||||
key={route.url}
|
|
||||||
to={route.url}
|
|
||||||
className={`${style.link} ${route.url === location.pathname ? style.current : ''}`}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{route.label}
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</NavigationMenu>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(ProductionNavigationMenu);
|
export default memo(ProductionNavigationMenu);
|
||||||
|
|||||||
@@ -1,28 +1,12 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
import { useFullscreen } from '@mantine/hooks';
|
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|
||||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
|
||||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
|
||||||
import { isKeyEnter } from '../../utils/keyEvent';
|
|
||||||
|
|
||||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
|
||||||
import FloatingNavigation from './FloatingNavigation';
|
import FloatingNavigation from './FloatingNavigation';
|
||||||
import NavigationMenu from './NavigationMenu';
|
import NavigationMenu from './NavigationMenu';
|
||||||
|
|
||||||
import style from './NavigationMenu.module.scss';
|
|
||||||
|
|
||||||
function ViewNavigationMenu() {
|
function ViewNavigationMenu() {
|
||||||
const location = useLocation();
|
|
||||||
const { fullscreen, toggle } = useFullscreen();
|
|
||||||
const { toggleMirror } = useViewOptionsStore();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { isOpen: isRenameOpen, onOpen: onRenameOpen, onClose: onRenameClose } = useDisclosure();
|
|
||||||
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
|
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
|
||||||
|
|
||||||
const showEditFormDrawer = useCallback(() => {
|
const showEditFormDrawer = useCallback(() => {
|
||||||
@@ -35,58 +19,7 @@ function ViewNavigationMenu() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
|
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
|
||||||
<NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose}>
|
<NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />
|
||||||
<RenameClientModal isOpen={isRenameOpen} onClose={onRenameClose} />
|
|
||||||
<div className={style.buttonsContainer}>
|
|
||||||
<div
|
|
||||||
className={style.link}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={toggle}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && toggle();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Toggle Fullscreen
|
|
||||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={style.link}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={() => toggleMirror()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && toggleMirror();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Flip Screen
|
|
||||||
<IoSwapVertical />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={style.link}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={onRenameOpen}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && onRenameOpen();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Rename Client
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className={style.separator} />
|
|
||||||
{navigatorConstants.map((route) => (
|
|
||||||
<Link
|
|
||||||
key={route.url}
|
|
||||||
to={route.url}
|
|
||||||
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{route.label}
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</NavigationMenu>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { HStack, IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
import { IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
||||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||||
|
|
||||||
import style from './ProtectRoute.module.scss';
|
import style from './ProtectRoute.module.scss';
|
||||||
@@ -39,8 +39,8 @@ export default function PinPage(props: PinPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
{`Ontime ${permission || ''}`}
|
{`Ontime ${permission}`}
|
||||||
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
|
<div className={failed ? style.pin__failed : style.pin}>
|
||||||
<PinInput
|
<PinInput
|
||||||
type='alphanumeric'
|
type='alphanumeric'
|
||||||
size='lg'
|
size='lg'
|
||||||
@@ -57,8 +57,15 @@ export default function PinPage(props: PinPageProps) {
|
|||||||
<PinInputField />
|
<PinInputField />
|
||||||
<PinInputField />
|
<PinInputField />
|
||||||
</PinInput>
|
</PinInput>
|
||||||
<IconButton aria-label='Enter' size='lg' isRound icon={<IoCheckmark />} onClick={validate} />
|
<IconButton
|
||||||
</HStack>
|
variant='ontime-filled'
|
||||||
|
aria-label='Enter'
|
||||||
|
size='lg'
|
||||||
|
isRound
|
||||||
|
icon={<IoCheckmark />}
|
||||||
|
onClick={validate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: flex;
|
||||||
place-content: center;
|
flex-direction: column;
|
||||||
height: 100vh;
|
align-items: center;
|
||||||
padding-bottom: 30vh;
|
padding-top: 25vh;
|
||||||
|
|
||||||
background: $bg-container-l1;
|
|
||||||
color: $ontime-color;
|
color: $ontime-color;
|
||||||
font-family: $ontime-font-family;
|
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
text-align: center;
|
font-size: 3rem;
|
||||||
font-size: 3vw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin,
|
.pin,
|
||||||
.pin__failed {
|
.pin__failed {
|
||||||
padding: 20px;
|
display: flex;
|
||||||
|
gap: 0.125em;
|
||||||
|
padding-block: 0.5em;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
border-radius: 50%;
|
border-radius: 99px;
|
||||||
|
border-color: $gray-500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $blue-500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin-left: 20px;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,9 +37,9 @@
|
|||||||
|
|
||||||
@keyframes colourFade {
|
@keyframes colourFade {
|
||||||
from {
|
from {
|
||||||
background: $action-blue;
|
background: $red-500;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
background: rgba($action-blue, 0);
|
background: rgba($red-500, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
.drawerFooter {
|
.drawerFooter {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: start;
|
justify-content: end;
|
||||||
gap: $section-spacing;
|
gap: $section-spacing;
|
||||||
|
|
||||||
button[type='reset'] {
|
button {
|
||||||
padding: 0 2em;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[type='submit'] {
|
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +11,9 @@
|
|||||||
.label {
|
.label {
|
||||||
font-size: $inner-section-text-size;
|
font-size: $inner-section-text-size;
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem
|
||||||
}
|
}
|
||||||
|
|
||||||
.columnSection {
|
.columnSection {
|
||||||
|
|||||||
@@ -52,15 +52,16 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
}
|
}
|
||||||
}, [searchParams, onOpen]);
|
}, [searchParams, onOpen]);
|
||||||
|
|
||||||
const onCloseWithoutSaving = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
|
||||||
|
|
||||||
searchParams.delete('edit');
|
searchParams.delete('edit');
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
|
|
||||||
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetParams = () => {
|
const resetParams = () => {
|
||||||
setSearchParams();
|
setSearchParams();
|
||||||
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||||
@@ -69,10 +70,12 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
|
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
|
||||||
setSearchParams(newSearchParams);
|
setSearchParams(newSearchParams);
|
||||||
|
|
||||||
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} variant='ontime' size='lg'>
|
<Drawer isOpen={isOpen} placement='right' onClose={handleClose} variant='ontime' size='lg'>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
@@ -96,10 +99,7 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
|
|
||||||
<DrawerFooter className={style.drawerFooter}>
|
<DrawerFooter className={style.drawerFooter}>
|
||||||
<Button variant='ontime-ghosted' onClick={resetParams} type='reset'>
|
<Button variant='ontime-ghosted' onClick={resetParams} type='reset'>
|
||||||
Reset
|
Reset to default
|
||||||
</Button>
|
|
||||||
<Button variant='ontime-subtle' onClick={onCloseWithoutSaving}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
||||||
Save
|
Save
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function useCustomFields() {
|
|||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: CUSTOM_FIELDS,
|
queryKey: CUSTOM_FIELDS,
|
||||||
queryFn: getCustomFields,
|
queryFn: getCustomFields,
|
||||||
placeholderData: placeholder,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchInterval,
|
refetchInterval: queryRefetchInterval,
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ export function useHttpSettings() {
|
|||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: HTTP_SETTINGS,
|
queryKey: HTTP_SETTINGS,
|
||||||
queryFn: getHTTP,
|
queryFn: getHTTP,
|
||||||
placeholderData: httpPlaceholder,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt: number) => attempt * 2500,
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
// we need to jump through some hoops because of the type op port
|
|
||||||
return { data: data ?? httpPlaceholder, status, isFetching, isError, refetch };
|
return { data: data ?? httpPlaceholder, status, isFetching, isError, refetch };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function useInfo() {
|
|||||||
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
|
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
|
||||||
queryKey: APP_INFO,
|
queryKey: APP_INFO,
|
||||||
queryFn: getInfo,
|
queryFn: getInfo,
|
||||||
placeholderData: ontimePlaceholderInfo,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function useOscSettings() {
|
|||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: OSC_SETTINGS,
|
queryKey: OSC_SETTINGS,
|
||||||
queryFn: getOSC,
|
queryFn: getOSC,
|
||||||
placeholderData: oscPlaceholderSettings,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt: number) => attempt * 2500,
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function useProjectData() {
|
|||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: PROJECT_DATA,
|
queryKey: PROJECT_DATA,
|
||||||
queryFn: getProjectData,
|
queryFn: getProjectData,
|
||||||
placeholderData: projectDataPlaceholder,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function useProjectList() {
|
|||||||
const { data, status, refetch } = useQuery({
|
const { data, status, refetch } = useQuery({
|
||||||
queryKey: PROJECT_LIST,
|
queryKey: PROJECT_LIST,
|
||||||
queryFn: getProjects,
|
queryFn: getProjects,
|
||||||
placeholderData: placeholderProjectList,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt: number) => attempt * 2500,
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function useRundown() {
|
|||||||
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
||||||
queryKey: RUNDOWN,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchNormalisedRundown,
|
queryFn: fetchNormalisedRundown,
|
||||||
placeholderData: cachedRundownPlaceholder,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchInterval,
|
refetchInterval: queryRefetchInterval,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function useSettings() {
|
|||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: APP_SETTINGS,
|
queryKey: APP_SETTINGS,
|
||||||
queryFn: getSettings,
|
queryFn: getSettings,
|
||||||
placeholderData: ontimePlaceholderSettings,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function useUrlPresets() {
|
|||||||
const { data, status, isError, refetch } = useQuery({
|
const { data, status, isError, refetch } = useQuery({
|
||||||
queryKey: URL_PRESETS,
|
queryKey: URL_PRESETS,
|
||||||
queryFn: getUrlPresets,
|
queryFn: getUrlPresets,
|
||||||
placeholderData: [],
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export default function useViewSettings() {
|
|||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
queryKey: VIEW_SETTINGS,
|
queryKey: VIEW_SETTINGS,
|
||||||
queryFn: getView,
|
queryFn: getView,
|
||||||
placeholderData: viewsSettingsPlaceholder,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data, status, isError, refetch, isFetching };
|
return { data: data ?? viewsSettingsPlaceholder, status, isError, refetch, isFetching };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function GeneralPanel({ location }: PanelBaseProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Settings</Panel.Header>
|
<Panel.Header>App Settings</Panel.Header>
|
||||||
<div ref={manageRef}>
|
<div ref={manageRef}>
|
||||||
<GeneralPanelForm />
|
<GeneralPanelForm />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'general',
|
id: 'general',
|
||||||
label: 'General',
|
label: 'App Settings',
|
||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'general__manage', label: 'Manage Ontime settings' },
|
{ id: 'general__manage', label: 'Manage Ontime settings' },
|
||||||
{ id: 'general__view', label: 'View settings' },
|
{ id: 'general__view', label: 'View settings' },
|
||||||
@@ -59,6 +59,7 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
||||||
|
|
||||||
export interface PanelBaseProps {
|
export interface PanelBaseProps {
|
||||||
location?: string;
|
location?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function MessageControl() {
|
|||||||
onClick={() => setMessage.timerBlink(!blink)}
|
onClick={() => setMessage.timerBlink(!blink)}
|
||||||
data-testid='toggle timer blink'
|
data-testid='toggle timer blink'
|
||||||
>
|
>
|
||||||
Blink message
|
Blink
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -66,7 +66,7 @@ export default function MessageControl() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<InputRow
|
<InputRow
|
||||||
label='External Message (readonly)'
|
label='External Message (read only)'
|
||||||
placeholder={enDash}
|
placeholder={enDash}
|
||||||
readonly
|
readonly
|
||||||
text={message.external.text || ''}
|
text={message.external.text || ''}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useDisclosure } from '@chakra-ui/react';
|
|||||||
|
|
||||||
import FloatingNavigation from '../../common/components/navigation-menu/FloatingNavigation';
|
import FloatingNavigation from '../../common/components/navigation-menu/FloatingNavigation';
|
||||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
|
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||||
|
|
||||||
import Operator from './Operator';
|
import Operator from './Operator';
|
||||||
|
|
||||||
@@ -19,10 +20,10 @@ export default function OperatorExport() {
|
|||||||
const toggleMenu = isOpen ? onClose : onOpen;
|
const toggleMenu = isOpen ? onClose : onOpen;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ProtectRoute permission='operator'>
|
||||||
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
|
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
|
||||||
<ProductionNavigationMenu isMenuOpen={isOpen} onMenuClose={onClose} />
|
<ProductionNavigationMenu isMenuOpen={isOpen} onMenuClose={onClose} />
|
||||||
<Operator />
|
<Operator />
|
||||||
</>
|
</ProtectRoute>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
|
|
||||||
.blackout {
|
.blackout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
|
|||||||
@@ -1664,7 +1664,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expectedEnd: 81600000, // 22:40:00
|
expectedEnd: 81600000, // 22:40:00
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: -200000,
|
||||||
current: -400000,
|
current: -400000,
|
||||||
duration: 3600000,
|
duration: 3600000,
|
||||||
elapsed: 4000000,
|
elapsed: 4000000,
|
||||||
@@ -1678,7 +1678,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(-400000);
|
expect(offset).toBe(400000); // <--- offset is always the overtime
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles time-to-end started after the end time', () => {
|
it('handles time-to-end started after the end time', () => {
|
||||||
@@ -1732,7 +1732,8 @@ describe('getRuntimeOffset()', () => {
|
|||||||
const updateCurrent = getCurrent(state);
|
const updateCurrent = getCurrent(state);
|
||||||
state.timer.current = updateCurrent;
|
state.timer.current = updateCurrent;
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(81000000 - 82000000); // <-- planned end - now
|
expect(millisToString(offset)).toBe('00:16:40');
|
||||||
|
expect(offset).toBe(82000000 - 81000000); // <-- now - planned end
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export function getRuntimeOffset(state: RuntimeState): MaybeNumber {
|
|||||||
return clock - timeStart;
|
return clock - timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overtime = Math.min(current, 0);
|
const overtime = Math.abs(Math.min(current, 0));
|
||||||
// in time-to-end, offset is overtime
|
// in time-to-end, offset is overtime
|
||||||
if (timerType === TimerType.TimeToEnd) {
|
if (timerType === TimerType.TimeToEnd) {
|
||||||
return overtime;
|
return overtime;
|
||||||
@@ -322,7 +322,7 @@ export function getRuntimeOffset(state: RuntimeState): MaybeNumber {
|
|||||||
const startOffset = startedAt - timeStart;
|
const startOffset = startedAt - timeStart;
|
||||||
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
||||||
|
|
||||||
return startOffset + addedTime + pausedTime + Math.abs(overtime);
|
return startOffset + addedTime + pausedTime + overtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ test('URL preset feature, it should redirect to given URL', async ({ page }) =>
|
|||||||
|
|
||||||
// open settings
|
// open settings
|
||||||
await page.getByRole('button', { name: 'Toggle settings' }).click();
|
await page.getByRole('button', { name: 'Toggle settings' }).click();
|
||||||
await page.getByRole('button', { name: 'General' }).click();
|
await page.getByRole('button', { name: 'App Settings' }).click();
|
||||||
|
|
||||||
// create preset
|
// create preset
|
||||||
await page.getByTestId('url-preset-form').scrollIntoViewIfNeeded();
|
await page.getByTestId('url-preset-form').scrollIntoViewIfNeeded();
|
||||||
|
|||||||
Reference in New Issue
Block a user