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:
Carlos Valente
2024-04-02 22:45:03 +02:00
committed by GitHub
parent 10852eecdd
commit 01c2ef4c4d
27 changed files with 184 additions and 212 deletions
+1
View File
@@ -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
}); });
}); });
+2 -2
View File
@@ -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;
} }
/** /**
+1 -1
View File
@@ -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();