mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
Navigation (#850)
This commit is contained in:
@@ -49,12 +49,12 @@ export default function AppRouter() {
|
|||||||
<Route path='/backstage' element={<SBackstage />} />
|
<Route path='/backstage' element={<SBackstage />} />
|
||||||
|
|
||||||
<Route path='/public' element={<SPublic />} />
|
<Route path='/public' element={<SPublic />} />
|
||||||
|
|
||||||
<Route path='/studio' element={<SStudio />} />
|
<Route path='/studio' element={<SStudio />} />
|
||||||
{/*/!* Lower cannot have fallback *!/*/}
|
|
||||||
<Route path='/lower' element={<SLowerThird />} />
|
<Route path='/lower' element={<SLowerThird />} />
|
||||||
|
|
||||||
<Route path='/op' element={<Operator />} />
|
<Route path='/op' element={<Operator />} />
|
||||||
<Route path='/operator' element={<Operator />} />
|
|
||||||
|
|
||||||
{/*/!* Protected Routes *!/*/}
|
{/*/!* Protected Routes *!/*/}
|
||||||
<Route path='/editor' element={<Editor />} />
|
<Route path='/editor' element={<Editor />} />
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import {
|
import { DatabaseModel, GetInfo, MessageResponse, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
||||||
DatabaseModel,
|
|
||||||
GetInfo,
|
|
||||||
MessageResponse,
|
|
||||||
ProjectData,
|
|
||||||
ProjectFileListResponse,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
@use '../../../theme/mixins' as *;
|
@use '../../../theme/mixins' as *;
|
||||||
|
|
||||||
$menu-bg: $gray-1200;
|
|
||||||
$menu-hover-bg: $gray-1350;
|
$menu-hover-bg: $gray-1350;
|
||||||
$menu-focus-bg: $gray-1300;
|
$menu-focus-bg: $gray-1300;
|
||||||
|
|
||||||
$icon-color: $ui-white;
|
$icon-color: $ui-white;
|
||||||
$button-bg: $gray-1050;
|
$button-bg: $gray-1050;
|
||||||
$button-size: 48px;
|
$button-size: 3rem;
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -21,7 +20,7 @@ $button-size: 48px;
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 12;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -29,7 +28,7 @@ $button-size: 48px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
font-size: 24px;
|
font-size: 1.5rem;
|
||||||
color: $icon-color;
|
color: $icon-color;
|
||||||
background-color: $button-bg;
|
background-color: $button-bg;
|
||||||
width: $button-size;
|
width: $button-size;
|
||||||
@@ -44,31 +43,10 @@ $button-size: 48px;
|
|||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuContainer {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: fit-content;
|
|
||||||
position: absolute;
|
|
||||||
background-color: $menu-bg;
|
|
||||||
min-width: 200px;
|
|
||||||
border-radius: 0 0 24px 0;
|
|
||||||
border-right: 1px solid $border-color-ondark;
|
|
||||||
|
|
||||||
box-shadow: $box-shadow-l2;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
|
|
||||||
max-height: 100vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonsContainer {
|
|
||||||
margin-top: calc(56px + 1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@include action-link;
|
@include action-link;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.75rem 1.5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $menu-hover-bg;
|
background-color: $menu-hover-bg;
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
import { memo, useEffect, useRef, useState } from 'react';
|
import { memo, PropsWithChildren, useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
import {
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
Drawer,
|
||||||
import { useFullscreen } from '@mantine/hooks';
|
DrawerBody,
|
||||||
|
DrawerCloseButton,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
|
||||||
import { IoPencilSharp } from '@react-icons/all-files/io5/IoPencilSharp';
|
|
||||||
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 { debounce } from '../../utils/debounce';
|
||||||
import { isKeyEnter } from '../../utils/keyEvent';
|
|
||||||
|
|
||||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
|
||||||
|
|
||||||
import style from './NavigationMenu.module.scss';
|
import style from './NavigationMenu.module.scss';
|
||||||
|
|
||||||
function NavigationMenu() {
|
interface NavigationMenuProps {
|
||||||
const location = useLocation();
|
editCallback: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenu(props: PropsWithChildren<NavigationMenuProps>) {
|
||||||
|
const { children, editCallback } = props;
|
||||||
|
|
||||||
const { fullscreen, toggle } = useFullscreen();
|
|
||||||
const { toggleMirror } = useViewOptionsStore();
|
|
||||||
const [showButton, setShowButton] = useState(false);
|
const [showButton, setShowButton] = useState(false);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useClickOutside(menuRef, () => setShowMenu(false));
|
useClickOutside(menuRef, () => onClose);
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const toggleMenu = () => (isOpen ? onClose() : onOpen());
|
||||||
|
|
||||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
|
||||||
|
|
||||||
// show on mouse move
|
// show on mouse move
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,98 +42,49 @@ function NavigationMenu() {
|
|||||||
}
|
}
|
||||||
fadeOut = setTimeout(() => setShowButton(false), 3000);
|
fadeOut = setTimeout(() => setShowButton(false), 3000);
|
||||||
};
|
};
|
||||||
document.addEventListener('mousemove', setShowMenuTrue);
|
|
||||||
|
const debouncedShowMenu = debounce(setShowMenuTrue, 1000);
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', debouncedShowMenu);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', setShowMenuTrue);
|
document.removeEventListener('mousemove', debouncedShowMenu);
|
||||||
if (fadeOut) {
|
if (fadeOut) {
|
||||||
clearTimeout(fadeOut);
|
clearTimeout(fadeOut);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFullscreen = () => toggle();
|
|
||||||
const handleMirror = () => toggleMirror();
|
|
||||||
|
|
||||||
const showEditFormDrawer = () => {
|
|
||||||
searchParams.set('edit', 'true');
|
|
||||||
setSearchParams(searchParams);
|
|
||||||
};
|
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div id='navigation-menu-portal' ref={menuRef}>
|
<div id='navigation-menu-portal' ref={menuRef}>
|
||||||
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
<div className={`${style.buttonContainer} ${!showButton && !isOpen ? style.hidden : ''}`}>
|
||||||
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
<button
|
||||||
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
onClick={toggleMenu}
|
||||||
|
aria-label='toggle menu'
|
||||||
|
className={style.navButton}
|
||||||
|
data-testid='navigation__toggle-menu'
|
||||||
|
>
|
||||||
<IoApps />
|
<IoApps />
|
||||||
</button>
|
</button>
|
||||||
<button className={style.button} onClick={showEditFormDrawer}>
|
<button
|
||||||
<IoPencilSharp />
|
className={style.button}
|
||||||
|
onClick={editCallback}
|
||||||
|
aria-label='toggle settings'
|
||||||
|
data-testid='navigation__toggle-settings'
|
||||||
|
>
|
||||||
|
<IoSettingsOutline />
|
||||||
</button>
|
</button>
|
||||||
{showMenu && (
|
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
||||||
<div className={style.menuContainer} data-testid='navigation-menu'>
|
<DrawerOverlay />
|
||||||
<div className={style.buttonsContainer}>
|
<DrawerContent>
|
||||||
<div
|
<DrawerHeader>
|
||||||
className={style.link}
|
<DrawerCloseButton size='lg' />
|
||||||
tabIndex={0}
|
Ontime
|
||||||
role='button'
|
</DrawerHeader>
|
||||||
onClick={handleFullscreen}
|
<DrawerBody padding={0}>{children}</DrawerBody>
|
||||||
onKeyDown={(event) => {
|
</DrawerContent>
|
||||||
isKeyEnter(event) && handleFullscreen();
|
</Drawer>
|
||||||
}}
|
|
||||||
>
|
|
||||||
Toggle Fullscreen
|
|
||||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={style.link}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
onClick={handleMirror}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && handleMirror();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Flip Screen
|
|
||||||
<IoSwapVertical />
|
|
||||||
</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='/cuesheet' className={style.link} tabIndex={0}>
|
|
||||||
Cuesheet
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
<Link to='/op' className={style.link} 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 : undefined}`}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{route.label}
|
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
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 style from './NavigationMenu.module.scss';
|
||||||
|
|
||||||
|
interface ProductionNavigationMenuProps {
|
||||||
|
handleSettings: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProductionNavigationMenu({ handleSettings }: ProductionNavigationMenuProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const { fullscreen, toggle } = useFullscreen();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationMenu editCallback={handleSettings}>
|
||||||
|
<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} ${'/editor' === location.pathname ? style.current : ''}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
Editor
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to='/cuesheet'
|
||||||
|
className={`${style.link} ${'/cuesheet' === location.pathname ? style.current : ''}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
Cuesheet
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</Link>
|
||||||
|
<Link to='/op' className={`${style.link} ${'/op' === location.pathname ? 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);
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { Link, useLocation, useSearchParams } 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 { 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 NavigationMenu from './NavigationMenu';
|
||||||
|
|
||||||
|
import style from './NavigationMenu.module.scss';
|
||||||
|
|
||||||
|
function ViewNavigationMenu() {
|
||||||
|
const location = useLocation();
|
||||||
|
const { fullscreen, toggle } = useFullscreen();
|
||||||
|
const { toggleMirror } = useViewOptionsStore();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
const showEditFormDrawer = useCallback(() => {
|
||||||
|
searchParams.set('edit', 'true');
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationMenu editCallback={showEditFormDrawer}>
|
||||||
|
<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={() => toggleMirror()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && toggleMirror();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Flip Screen
|
||||||
|
<IoSwapVertical />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={style.link}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={onOpen}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && onOpen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ViewNavigationMenu);
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
.modalBody {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
+3
-5
@@ -13,8 +13,6 @@ import {
|
|||||||
import { setClientName } from '../../../hooks/useSocket';
|
import { setClientName } from '../../../hooks/useSocket';
|
||||||
import { useSocketClientName } from '../../../stores/connectionName';
|
import { useSocketClientName } from '../../../stores/connectionName';
|
||||||
|
|
||||||
import style from './RenameClientModal.module.scss';
|
|
||||||
|
|
||||||
interface RenameClientModalProps {
|
interface RenameClientModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -45,18 +43,18 @@ export default function RenameClientModal({ isOpen, onClose }: RenameClientModal
|
|||||||
motionPreset='slideInBottom'
|
motionPreset='slideInBottom'
|
||||||
scrollBehavior='inside'
|
scrollBehavior='inside'
|
||||||
preserveScrollBarGap
|
preserveScrollBarGap
|
||||||
variant='ontime-small'
|
variant='ontime'
|
||||||
>
|
>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Rename client</ModalHeader>
|
<ModalHeader>Rename client</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody className={style.modalBody}>
|
<ModalBody>
|
||||||
<Input
|
<Input
|
||||||
placeholder='Connection must have a name'
|
placeholder='Connection must have a name'
|
||||||
defaultValue={newName}
|
defaultValue={newName}
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
variant='ontime-filled-on-light'
|
variant='ontime-filled'
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={newName === clientName || !newName}
|
isDisabled={newName === clientName || !newName}
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
.drawerContent {
|
|
||||||
background-color: $gray-1250;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerHeader {
|
|
||||||
@extend .drawerContent;
|
|
||||||
color: $section-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerFooter {
|
.drawerFooter {
|
||||||
@extend .drawerContent;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
gap: $section-spacing;
|
gap: $section-spacing;
|
||||||
|
|||||||
@@ -72,15 +72,15 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} size='lg'>
|
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} variant='ontime' size='lg'>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader className={style.drawerHeader}>
|
<DrawerHeader>
|
||||||
<DrawerCloseButton _hover={{ bg: '#ebedf0', color: '#333' }} size='lg' />
|
<DrawerCloseButton size='lg' />
|
||||||
Customise
|
Customise
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
|
|
||||||
<DrawerBody className={style.drawerContent}>
|
<DrawerBody>
|
||||||
<form id='edit-params-form' onSubmit={onParamsFormSubmit}>
|
<form id='edit-params-form' onSubmit={onParamsFormSubmit}>
|
||||||
{paramFields.map((field) => (
|
{paramFields.map((field) => (
|
||||||
<div key={field.title} className={style.columnSection}>
|
<div key={field.title} className={style.columnSection}>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import InterfacePanel from './panel/interface-panel/InterfacePanel';
|
|||||||
import LogPanel from './panel/log-panel/LogPanel';
|
import LogPanel from './panel/log-panel/LogPanel';
|
||||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||||
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
||||||
|
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
||||||
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||||
import PanelContent from './panel-content/PanelContent';
|
import PanelContent from './panel-content/PanelContent';
|
||||||
import PanelList from './panel-list/PanelList';
|
import PanelList from './panel-list/PanelList';
|
||||||
@@ -33,6 +34,7 @@ export default function AppSettings() {
|
|||||||
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
||||||
{panel === 'about' && <AboutPanel />}
|
{panel === 'about' && <AboutPanel />}
|
||||||
{panel === 'log' && <LogPanel />}
|
{panel === 'log' && <LogPanel />}
|
||||||
|
{panel === 'shutdown' && <ShutdownPanel />}
|
||||||
</PanelContent>
|
</PanelContent>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import useElectronEvent from '../../../../common/hooks/useElectronEvent';
|
||||||
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
export default function ShutdownPanel() {
|
||||||
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
|
|
||||||
|
const sendShutdown = () => {
|
||||||
|
sendToElectron('shutdown', 'now');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel.Header>Shutdown Ontime</Panel.Header>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Paragraph>
|
||||||
|
This will shutdown the Ontime server. <br />
|
||||||
|
The runtime state will be lost, but your project is kept for next time.
|
||||||
|
</Panel.Paragraph>
|
||||||
|
<Button colorScheme='red' onClick={sendShutdown} isDisabled={!isElectron}>
|
||||||
|
Shutdown ontime
|
||||||
|
</Button>
|
||||||
|
</Panel.Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -51,6 +51,11 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
label: 'About',
|
label: 'About',
|
||||||
split: true,
|
split: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'shutdown',
|
||||||
|
label: 'Shutdown',
|
||||||
|
split: true,
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
||||||
|
|||||||
@@ -4,18 +4,11 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto 1fr;
|
grid-template-rows: 3rem auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'header'
|
'overview'
|
||||||
'settings'
|
'settings'
|
||||||
'table';
|
'table';
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
color: $ui-white;
|
||||||
background-color: $gray-1300;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
border: 1px solid $white-10;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { CustomFieldLabel, isOntimeEvent, ProjectData } from 'ontime-types';
|
import { CustomFieldLabel, isOntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import { useCuesheet } from '../../common/hooks/useSocket';
|
import { useCuesheet } from '../../common/hooks/useSocket';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||||
|
import Overview from '../overview/Overview';
|
||||||
|
|
||||||
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||||
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
|
import { useCuesheetSettings } from './store/CuesheetSettings';
|
||||||
import Cuesheet from './Cuesheet';
|
import Cuesheet from './Cuesheet';
|
||||||
import { makeCuesheetColumns } from './cuesheetCols';
|
import { makeCuesheetColumns } from './cuesheetCols';
|
||||||
import { makeCSV, makeTable } from './cuesheetUtils';
|
|
||||||
|
|
||||||
import styles from './CuesheetWrapper.module.scss';
|
import styles from './CuesheetWrapper.module.scss';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export default function CuesheetWrapper() {
|
|||||||
const { updateCustomField } = useEventAction();
|
const { updateCustomField } = useEventAction();
|
||||||
const featureData = useCuesheet();
|
const featureData = useCuesheet();
|
||||||
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
||||||
|
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -74,39 +76,15 @@ export default function CuesheetWrapper() {
|
|||||||
[flatRundown, rundownStatus, updateCustomField],
|
[flatRundown, rundownStatus, updateCustomField],
|
||||||
);
|
);
|
||||||
|
|
||||||
const exportHandler = useCallback(
|
|
||||||
(headerData: ProjectData) => {
|
|
||||||
if (!flatRundown || rundownStatus !== 'success') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sheetData = makeTable(headerData, flatRundown, customFields);
|
|
||||||
const csvContent = makeCSV(sheetData);
|
|
||||||
|
|
||||||
const fileName = 'ontime rundown.csv';
|
|
||||||
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.setAttribute('href', url);
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
// Clean up the URL.createObjectURL to release resources
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
[flatRundown, rundownStatus, customFields],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!customFields || !flatRundown || rundownStatus !== 'success') {
|
if (!customFields || !flatRundown || rundownStatus !== 'success') {
|
||||||
return <Empty text='Loading...' />;
|
return <Empty text='Loading...' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||||
<CuesheetTableHeader handleExport={exportHandler} featureData={featureData} />
|
<Overview />
|
||||||
<CuesheetProgress />
|
<CuesheetProgress />
|
||||||
|
<ProductionNavigationMenu handleSettings={() => toggleSettings()} />
|
||||||
<Cuesheet
|
<Cuesheet
|
||||||
data={flatRundown}
|
data={flatRundown}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { useFullscreen } from '@mantine/hooks';
|
|
||||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
|
||||||
import { IoLocate } from '@react-icons/all-files/io5/IoLocate';
|
import { IoLocate } from '@react-icons/all-files/io5/IoLocate';
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
|
||||||
import { Playback, ProjectData } from 'ontime-types';
|
import { Playback, ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
||||||
@@ -28,10 +24,7 @@ interface CuesheetTableHeaderProps {
|
|||||||
|
|
||||||
export default function CuesheetTableHeader({ handleExport, featureData }: CuesheetTableHeaderProps) {
|
export default function CuesheetTableHeader({ handleExport, featureData }: CuesheetTableHeaderProps) {
|
||||||
const followSelected = useCuesheetSettings((state) => state.followSelected);
|
const followSelected = useCuesheetSettings((state) => state.followSelected);
|
||||||
const showSettings = useCuesheetSettings((state) => state.showSettings);
|
|
||||||
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
|
|
||||||
const toggleFollow = useCuesheetSettings((state) => state.toggleFollow);
|
const toggleFollow = useCuesheetSettings((state) => state.toggleFollow);
|
||||||
const { fullscreen, toggle } = useFullscreen();
|
|
||||||
const { data: project } = useProjectData();
|
const { data: project } = useProjectData();
|
||||||
|
|
||||||
const exportProject = () => {
|
const exportProject = () => {
|
||||||
@@ -66,19 +59,6 @@ export default function CuesheetTableHeader({ handleExport, featureData }: Cuesh
|
|||||||
<IoLocate />
|
<IoLocate />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle settings'>
|
|
||||||
<span
|
|
||||||
onClick={() => toggleSettings()}
|
|
||||||
className={cx([style.actionIcon, showSettings ? style.enabled : null])}
|
|
||||||
>
|
|
||||||
<IoSettingsOutline />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle Fullscreen'>
|
|
||||||
<span onClick={() => toggle()} className={style.actionIcon}>
|
|
||||||
{fullscreen ? <IoContract /> : <IoExpand />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Export rundown'>
|
<Tooltip openDelay={tooltipDelayFast} label='Export rundown'>
|
||||||
<span className={style.actionText} onClick={exportProject}>
|
<span className={style.actionText} onClick={exportProject}>
|
||||||
Export CSV
|
Export CSV
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ interface CuesheetTableSettingsProps {
|
|||||||
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
||||||
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
|
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
|
||||||
const {
|
const {
|
||||||
|
followSelected,
|
||||||
|
toggleFollow,
|
||||||
showPrevious,
|
showPrevious,
|
||||||
toggleDelayVisibility,
|
togglePreviousVisibility,
|
||||||
showDelayBlock,
|
showDelayBlock,
|
||||||
showDelayedTimes,
|
showDelayedTimes,
|
||||||
toggleDelayedTimes,
|
toggleDelayedTimes,
|
||||||
togglePreviousVisibility,
|
toggleDelayVisibility,
|
||||||
} = useCuesheetSettings();
|
} = useCuesheetSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,6 +55,10 @@ function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={style.sectionTitle}>Table Options</div>
|
<div className={style.sectionTitle}>Table Options</div>
|
||||||
<div className={style.options}>
|
<div className={style.options}>
|
||||||
|
<label className={style.option}>
|
||||||
|
<Switch variant='ontime' size='sm' isChecked={followSelected} onChange={() => toggleFollow()} />
|
||||||
|
Follow selected event
|
||||||
|
</label>
|
||||||
<label className={style.option}>
|
<label className={style.option}>
|
||||||
<Switch variant='ontime' size='sm' isChecked={showPrevious} onChange={() => togglePreviousVisibility()} />
|
<Switch variant='ontime' size='sm' isChecked={showPrevious} onChange={() => togglePreviousVisibility()} />
|
||||||
Show past events
|
Show past events
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
@use './EditorMixin' as editor;
|
@use './EditorMixin' as editor;
|
||||||
|
|
||||||
$menu-width: 2.75rem;
|
|
||||||
$min-playback-width: 27rem;
|
$min-playback-width: 27rem;
|
||||||
$max-playback-width: 35rem;
|
$max-playback-width: 35rem;
|
||||||
$panel-gap: 0.5rem;
|
$panel-gap: 0.5rem;
|
||||||
@@ -17,11 +16,11 @@ $panel-gap: 0.5rem;
|
|||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: $menu-width auto;
|
grid-template-columns: auto;
|
||||||
grid-template-rows: 3rem 1fr;
|
grid-template-rows: 3rem 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'menu overview'
|
'overview'
|
||||||
'menu main';
|
'main';
|
||||||
gap: $panel-gap;
|
gap: $panel-gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { lazy, useEffect } from 'react';
|
import { lazy, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
|
import useElectronEvent from '../../common/hooks/useElectronEvent';
|
||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import { SettingsOptionId } from '../app-settings/settingsStore';
|
|
||||||
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
||||||
import MenuBar from '../menu/MenuBar';
|
|
||||||
import Overview from '../overview/Overview';
|
import Overview from '../overview/Overview';
|
||||||
|
|
||||||
import styles from './Editor.module.scss';
|
import styles from './Editor.module.scss';
|
||||||
@@ -15,14 +14,46 @@ const MessageControl = lazy(() => import('../control/message/MessageControlExpor
|
|||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const { isOpen, setLocation, close } = useAppSettingsNavigation();
|
const { isOpen, setLocation, close } = useAppSettingsNavigation();
|
||||||
|
const { isElectron } = useElectronEvent();
|
||||||
|
|
||||||
const handleSettings = (newTab?: SettingsOptionId) => {
|
const handleSettings = useCallback(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
close();
|
close();
|
||||||
} else {
|
} else {
|
||||||
setLocation(newTab ?? 'project');
|
setLocation('project');
|
||||||
}
|
}
|
||||||
};
|
}, [close, isOpen, setLocation]);
|
||||||
|
|
||||||
|
// Handle keyboard shortcuts
|
||||||
|
const handleKeyPress = useCallback(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
// handle held key
|
||||||
|
if (event.repeat) return;
|
||||||
|
|
||||||
|
// check if the ctrl key is pressed
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
// ctrl + , (settings)
|
||||||
|
if (event.key === ',') {
|
||||||
|
handleSettings();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
// register ctrl + , to open settings
|
||||||
|
useEffect(() => {
|
||||||
|
if (isElectron) {
|
||||||
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (isElectron) {
|
||||||
|
document.removeEventListener('keydown', handleKeyPress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [handleKeyPress, isElectron]);
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,9 +62,7 @@ export default function Editor() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
<ErrorBoundary>
|
<ProductionNavigationMenu handleSettings={handleSettings} />
|
||||||
<MenuBar openSettings={handleSettings} isSettingsOpen={isOpen} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<AppSettings />
|
<AppSettings />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.menu {
|
|
||||||
grid-area: menu;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
padding-bottom: 1rem
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap {
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.open {
|
|
||||||
background: $blue-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { memo, useCallback, useEffect } from 'react';
|
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
|
||||||
|
|
||||||
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
|
|
||||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
|
||||||
import useElectronEvent from '../../common/hooks/useElectronEvent';
|
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
|
||||||
|
|
||||||
import style from './MenuBar.module.scss';
|
|
||||||
|
|
||||||
interface MenuBarProps {
|
|
||||||
openSettings: (newTab?: string) => void;
|
|
||||||
isSettingsOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonStyle = {
|
|
||||||
fontSize: '1.25em',
|
|
||||||
size: 'md',
|
|
||||||
colorScheme: 'white',
|
|
||||||
_hover: {
|
|
||||||
background: 'rgba(255, 255, 255, 0.10)', // $white-10
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
background: 'rgba(255, 255, 255, 0.13)', // $white-13
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuBar = (props: MenuBarProps) => {
|
|
||||||
const { openSettings, isSettingsOpen } = props;
|
|
||||||
const { isElectron, sendToElectron } = useElectronEvent();
|
|
||||||
|
|
||||||
const sendShutdown = () => {
|
|
||||||
if (isElectron) {
|
|
||||||
sendToElectron('shutdown', 'now');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
|
||||||
const handleKeyPress = useCallback(
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
// handle held key
|
|
||||||
if (event.repeat) return;
|
|
||||||
|
|
||||||
// check if the ctrl key is pressed
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
|
||||||
// ctrl + , (settings)
|
|
||||||
if (event.key === ',') {
|
|
||||||
openSettings();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[openSettings],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isElectron) {
|
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (isElectron) {
|
|
||||||
document.removeEventListener('keydown', handleKeyPress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [handleKeyPress, isElectron]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.menu}>
|
|
||||||
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
|
|
||||||
<TooltipActionBtn
|
|
||||||
{...buttonStyle}
|
|
||||||
className={cx([isSettingsOpen ? style.open : null, style.bottom])}
|
|
||||||
icon={<IoSettingsOutline />}
|
|
||||||
clickHandler={() => openSettings()}
|
|
||||||
tooltip='Application settings'
|
|
||||||
aria-label='Application settings'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(MenuBar);
|
|
||||||
@@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { CustomField, CustomFields, isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types';
|
import { CustomField, CustomFields, isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils';
|
import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils';
|
||||||
|
|
||||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import { getOperatorOptions } from '../../common/components/view-params-editor/constants';
|
import { getOperatorOptions } from '../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
@@ -41,7 +41,7 @@ export default function Operator() {
|
|||||||
const timeoutId = useRef<NodeJS.Timeout | null>(null);
|
const timeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const featureData = useOperator();
|
const featureData = useOperator();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { data: settings } = useSettings();
|
const { data: settings } = useSettings();
|
||||||
|
|
||||||
const [showEditPrompt, setShowEditPrompt] = useState(false);
|
const [showEditPrompt, setShowEditPrompt] = useState(false);
|
||||||
@@ -116,6 +116,11 @@ export default function Operator() {
|
|||||||
[searchParams],
|
[searchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showEditFormDrawer = useCallback(() => {
|
||||||
|
searchParams.set('edit', 'true');
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
const missingData = !data || !customFields || !projectData;
|
const missingData = !data || !customFields || !projectData;
|
||||||
const isLoading = status === 'pending' || customFieldStatus === 'pending' || projectDataStatus === 'pending';
|
const isLoading = status === 'pending' || customFieldStatus === 'pending' || projectDataStatus === 'pending';
|
||||||
|
|
||||||
@@ -141,7 +146,7 @@ export default function Operator() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.operatorContainer}>
|
<div className={style.operatorContainer}>
|
||||||
<NavigationMenu />
|
<ProductionNavigationMenu handleSettings={showEditFormDrawer} />
|
||||||
<ViewParamsEditor paramFields={operatorOptions} />
|
<ViewParamsEditor paramFields={operatorOptions} />
|
||||||
{editEvent && <EditModal event={editEvent} onClose={() => setEditEvent(null)} />}
|
{editEvent && <EditModal event={editEvent} onClose={() => setEditEvent(null)} />}
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,18 @@
|
|||||||
import { useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { MaybeNumber } from 'ontime-types';
|
|
||||||
import { dayInMs, millisToString } from 'ontime-utils';
|
|
||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import { useRuntimeOverview, useRuntimePlaybackOverview } from '../../common/hooks/useSocket';
|
import { useRuntimeOverview, useRuntimePlaybackOverview } from '../../common/hooks/useSocket';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import { enDash, timerPlaceholder } from '../../common/utils/styleUtils';
|
import { enDash } from '../../common/utils/styleUtils';
|
||||||
|
|
||||||
import { TimeColumn, TimeRow } from './composite/TimeLayout';
|
import { TimeColumn, TimeRow } from './composite/TimeLayout';
|
||||||
|
import { calculateEndAndDaySpan, formatedTime, getOffsetText } from './overviewUtils';
|
||||||
|
|
||||||
import style from './Overview.module.scss';
|
import style from './Overview.module.scss';
|
||||||
|
|
||||||
/**
|
export default memo(Overview);
|
||||||
* Encapsulates the logic for formatting time in overview
|
|
||||||
* @param time
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function formatedTime(time: MaybeNumber) {
|
|
||||||
return millisToString(time, { fallback: timerPlaceholder });
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateEndAndDaySpan(end: MaybeNumber): [MaybeNumber, number] {
|
function Overview() {
|
||||||
let maybeEnd = end;
|
|
||||||
let maybeDaySpan = 0;
|
|
||||||
if (end !== null) {
|
|
||||||
if (end > dayInMs) {
|
|
||||||
maybeEnd = end % dayInMs;
|
|
||||||
maybeDaySpan = Math.floor(end / dayInMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [maybeEnd, maybeDaySpan];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Overview() {
|
|
||||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||||
|
|
||||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
||||||
@@ -70,18 +50,6 @@ function TitlesOverview() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOffsetText(offset: MaybeNumber): string {
|
|
||||||
if (offset === null) {
|
|
||||||
return enDash;
|
|
||||||
}
|
|
||||||
const isAhead = offset <= 0;
|
|
||||||
let offsetText = millisToString(Math.abs(offset), { fallback: enDash });
|
|
||||||
if (offsetText !== enDash) {
|
|
||||||
offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`;
|
|
||||||
}
|
|
||||||
return offsetText;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RuntimeOverview() {
|
function RuntimeOverview() {
|
||||||
const { clock, numEvents, selectedEventIndex, offset } = useRuntimePlaybackOverview();
|
const { clock, numEvents, selectedEventIndex, offset } = useRuntimePlaybackOverview();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
import { dayInMs, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { enDash, timerPlaceholder } from '../../common/utils/styleUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates the logic for formatting time in overview
|
||||||
|
* @param time
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function formatedTime(time: MaybeNumber) {
|
||||||
|
return millisToString(time, { fallback: timerPlaceholder });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a day span from a number range
|
||||||
|
* @param end
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function calculateEndAndDaySpan(end: MaybeNumber): [MaybeNumber, number] {
|
||||||
|
let maybeEnd = end;
|
||||||
|
let maybeDaySpan = 0;
|
||||||
|
if (end !== null) {
|
||||||
|
if (end > dayInMs) {
|
||||||
|
maybeEnd = end % dayInMs;
|
||||||
|
maybeDaySpan = Math.floor(end / dayInMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [maybeEnd, maybeDaySpan];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats offset text
|
||||||
|
* @param offset
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getOffsetText(offset: MaybeNumber): string {
|
||||||
|
if (offset === null) {
|
||||||
|
return enDash;
|
||||||
|
}
|
||||||
|
const isAhead = offset <= 0;
|
||||||
|
let offsetText = millisToString(Math.abs(offset), { fallback: enDash });
|
||||||
|
if (offsetText !== enDash) {
|
||||||
|
offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`;
|
||||||
|
}
|
||||||
|
return offsetText;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
|
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||||
@@ -93,29 +94,32 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<>
|
||||||
{...props}
|
<ViewNavigationMenu />
|
||||||
backstageEvents={rundownData}
|
<Component
|
||||||
customFields={customFields}
|
{...props}
|
||||||
eventNext={eventNext}
|
backstageEvents={rundownData}
|
||||||
eventNow={eventNow}
|
customFields={customFields}
|
||||||
events={publicEvents}
|
eventNext={eventNext}
|
||||||
external={message.external}
|
eventNow={eventNow}
|
||||||
general={project}
|
events={publicEvents}
|
||||||
isMirrored={isMirrored}
|
external={message.external}
|
||||||
lower={message.lower}
|
general={project}
|
||||||
nextId={nextId}
|
isMirrored={isMirrored}
|
||||||
onAir={onAir}
|
lower={message.lower}
|
||||||
pres={message.timer}
|
nextId={nextId}
|
||||||
publ={message.public}
|
onAir={onAir}
|
||||||
publicEventNext={publicEventNext}
|
pres={message.timer}
|
||||||
publicEventNow={publicEventNow}
|
publ={message.public}
|
||||||
publicSelectedId={publicSelectedId}
|
publicEventNext={publicEventNext}
|
||||||
selectedId={selectedId}
|
publicEventNow={publicEventNow}
|
||||||
settings={settings}
|
publicSelectedId={publicSelectedId}
|
||||||
time={TimeManagerType}
|
selectedId={selectedId}
|
||||||
viewSettings={viewSettings}
|
settings={settings}
|
||||||
/>
|
time={TimeManagerType}
|
||||||
|
viewSettings={viewSettings}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { CustomFields, Message, OntimeEvent, ProjectData, Settings, SupportedEve
|
|||||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
import Schedule from '../../../common/components/schedule/Schedule';
|
||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
@@ -102,7 +101,6 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={backstageOptions} />
|
<ViewParamsEditor paramFields={backstageOptions} />
|
||||||
<div className='project-header'>
|
<div className='project-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { Settings, ViewSettings } from 'ontime-types';
|
import { Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
import { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
@@ -133,7 +132,6 @@ export default function Clock(props: ClockProps) {
|
|||||||
}}
|
}}
|
||||||
data-testid='clock-view'
|
data-testid='clock-view'
|
||||||
>
|
>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={clockOptions} />
|
<ViewParamsEditor paramFields={clockOptions} />
|
||||||
<SuperscriptTime
|
<SuperscriptTime
|
||||||
time={clock}
|
time={clock}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, Vi
|
|||||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
@@ -110,7 +109,6 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={timeOption} />
|
<ViewParamsEditor paramFields={timeOption} />
|
||||||
{follow === null ? (
|
{follow === null ? (
|
||||||
<CountdownSelect events={backstageEvents} />
|
<CountdownSelect events={backstageEvents} />
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { CustomFields, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
import { CustomFields, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
|
import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
@@ -191,7 +190,6 @@ export default function LowerThird(props: LowerProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='lower-third' style={{ backgroundColor: `#${options.key}` }}>
|
<div className='lower-third' style={{ backgroundColor: `#${options.key}` }}>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={getLowerThirdOptions(customFields)} />
|
<ViewParamsEditor paramFields={getLowerThirdOptions(customFields)} />
|
||||||
<div
|
<div
|
||||||
className={`container container--${playState}`}
|
className={`container container--${playState}`}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
|||||||
import { MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
import { MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import { MINIMAL_TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
import { MINIMAL_TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
@@ -186,7 +185,6 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
|||||||
}}
|
}}
|
||||||
data-testid='minimal-timer'
|
data-testid='minimal-timer'
|
||||||
>
|
>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={MINIMAL_TIMER_OPTIONS} />
|
<ViewParamsEditor paramFields={MINIMAL_TIMER_OPTIONS} />
|
||||||
{!hideMessagesOverlay && (
|
{!hideMessagesOverlay && (
|
||||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|||||||
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
import Schedule from '../../../common/components/schedule/Schedule';
|
||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||||
@@ -78,7 +77,6 @@ export default function Public(props: BackstageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={publicOptions} />
|
<ViewParamsEditor paramFields={publicOptions} />
|
||||||
<div className='project-header'>
|
<div className='project-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { isOntimeEvent, Playback } from 'ontime-types';
|
|||||||
import { millisToString, removeSeconds } from 'ontime-utils';
|
import { millisToString, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import useFitText from '../../../common/hooks/useFitText';
|
import useFitText from '../../../common/hooks/useFitText';
|
||||||
@@ -78,7 +77,6 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={studioClockOptions} />
|
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
{hasAmPm && <div className='clock__ampm'>{hasAmPm}</div>}
|
{hasAmPm && <div className='clock__ampm'>{hasAmPm}</div>}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } f
|
|||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { getTimerOptions } from '../../../common/components/view-params-editor/constants';
|
import { getTimerOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
@@ -162,7 +161,6 @@ export default function Timer(props: TimerProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||||
<NavigationMenu />
|
|
||||||
<ViewParamsEditor paramFields={timerOptions} />
|
<ViewParamsEditor paramFields={timerOptions} />
|
||||||
<div className={showBlackout ? 'blackout blackout--active' : 'blackout'} />
|
<div className={showBlackout ? 'blackout blackout--active' : 'blackout'} />
|
||||||
{!userOptions.hideMessage && (
|
{!userOptions.hideMessage && (
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
export const ontimeAlertOnLight = {
|
|
||||||
container: {
|
|
||||||
fontSize: '14px',
|
|
||||||
backgroundColor: '#f6f6f6', // $gray-50
|
|
||||||
color: '#101010', // $ui-black
|
|
||||||
borderRadius: '3px',
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
color: '#578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeAlertOnDark = {
|
export const ontimeAlertOnDark = {
|
||||||
container: {
|
container: {
|
||||||
fontSize: 'calc(1rem - 1px)',
|
fontSize: 'calc(1rem - 1px)',
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export const ontimeProgressGray = {
|
|
||||||
track: {
|
|
||||||
background: '#f6f6f6', // $gray-500
|
|
||||||
},
|
|
||||||
filledTrack: {
|
|
||||||
background: '#578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -71,38 +71,6 @@ export const ontimeButtonGhosted = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ontimeButtonSubtleOnLight = {
|
|
||||||
backgroundColor: '#ececec', // $gray-100
|
|
||||||
color: '#595959', // $gray-800
|
|
||||||
border: '1px solid transparent',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: '#cfcfcf', // $gray-200
|
|
||||||
_disabled: {
|
|
||||||
backgroundColor: '#ececec', // $gray-100
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
backgroundColor: '#ececec', // $gray-200
|
|
||||||
borderColor: '#ececec', // $gray-300
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeGhostOnLight = {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
color: '#595959', // $gray-800
|
|
||||||
_hover: {
|
|
||||||
color: '#595959', // $gray-800
|
|
||||||
backgroundColor: '#ececec', // $gray-200
|
|
||||||
_disabled: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
borderColor: '#595959', // $gray-800
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeButtonSubtleWhite = {
|
export const ontimeButtonSubtleWhite = {
|
||||||
...ontimeButtonSubtle,
|
...ontimeButtonSubtle,
|
||||||
color: '#f6f6f6', // $gray-50
|
color: '#f6f6f6', // $gray-50
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export const ontimeDrawer = {
|
||||||
|
header: {
|
||||||
|
color: '#fefefe', // $gray-50
|
||||||
|
backgroundColor: '#202020', // $gray-1250
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
color: '#fefefe', // $gray-50
|
||||||
|
backgroundColor: '#202020', // $gray-1250
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
backgroundColor: '#202020', // $gray-1250
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
color: '#fefefe', // $gray-50
|
||||||
|
_hover: {
|
||||||
|
color: '#303030', // $gray-1050
|
||||||
|
backgroundColor: '#fefefe', // $gray-50
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,45 +4,26 @@ export const ontimeModal = {
|
|||||||
letterSpacing: '0.3px',
|
letterSpacing: '0.3px',
|
||||||
padding: '1rem 1.5rem',
|
padding: '1rem 1.5rem',
|
||||||
fontSize: '1.25rem',
|
fontSize: '1.25rem',
|
||||||
color: '#202020', // $gray-50
|
color: '#fefefe', // $gray-50
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
minHeight: 'min(500px, 75vh)',
|
minHeight: 'min(200px, 10vh)',
|
||||||
|
backgroundColor: '#202020', // $gray-1250
|
||||||
|
color: '#fefefe', // $gray-50
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
padding: 0,
|
padding: '1rem',
|
||||||
|
fontSize: 'calc(1rem - 2px)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
gap: '1rem',
|
||||||
},
|
},
|
||||||
closeButton: {
|
closeButton: {
|
||||||
color: '#202020', // $gray-50
|
color: '#fefefe', // $gray-50
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ontimeSmallModal = {
|
|
||||||
...ontimeModal,
|
|
||||||
body: {
|
|
||||||
padding: '1rem',
|
|
||||||
fontSize: 'calc(1rem - 2px)',
|
|
||||||
},
|
|
||||||
dialog: {
|
|
||||||
minHeight: 'min(200px, 10vh)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeUploadModal = {
|
|
||||||
...ontimeSmallModal,
|
|
||||||
body: {
|
|
||||||
padding: '1rem',
|
|
||||||
fontSize: 'calc(1rem - 2px)',
|
|
||||||
},
|
|
||||||
dialog: {
|
|
||||||
minHeight: 'min(200px, 10vh)',
|
|
||||||
maxWidth: 'min(800px, 80vh)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -10,16 +10,3 @@ export const ontimeSwitch = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lightSwitch = {
|
|
||||||
track: {
|
|
||||||
border: '1px solid transparent',
|
|
||||||
background: '#cfcfcf', // $gray-300
|
|
||||||
_checked: {
|
|
||||||
background: '#578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
border: '1px solid #D2DDFF', // $blue-200
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -38,27 +38,10 @@ export const ontimeInputGhosted = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ontimeInputFilledOnLight = {
|
|
||||||
field: {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: '2px solid #f6f6f6', // $gray-50
|
|
||||||
_hover: {
|
|
||||||
border: '2px solid #D2DDFF', // $blue-200
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
border: '2px solid #578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
_disabled: {
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeTextAreaFilled = {
|
export const ontimeTextAreaFilled = {
|
||||||
...commonStyles,
|
...commonStyles,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ontimeTextAreaTransparent = {
|
export const ontimeTextAreaTransparent = {
|
||||||
...commonStyles,
|
...commonStyles,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
@@ -66,24 +49,3 @@ export const ontimeTextAreaTransparent = {
|
|||||||
backgroundColor: 'rgba(255, 255, 255, 0.10)', // $white-10
|
backgroundColor: 'rgba(255, 255, 255, 0.10)', // $white-10
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ontimeTextAreaFilledOnLight = {
|
|
||||||
borderRadius: '3px',
|
|
||||||
fontWeight: '400',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
color: '#202020', // $gray-1200
|
|
||||||
border: '2px solid #f6f6f6', // $gray-50
|
|
||||||
_hover: {
|
|
||||||
border: '2px solid #D2DDFF', // $blue-200
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
color: '#101010',
|
|
||||||
border: '2px solid #578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
_placeholder: { color: '#9d9d9d' }, // $gray-500
|
|
||||||
_disabled: {
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import { extendTheme } from '@chakra-ui/react';
|
import { extendTheme } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { ontimeAlertOnDark, ontimeAlertOnLight } from './OntimeAlert';
|
import { ontimeAlertOnDark } from './OntimeAlert';
|
||||||
import {
|
import {
|
||||||
ontimeButtonFilled,
|
ontimeButtonFilled,
|
||||||
ontimeButtonGhosted,
|
ontimeButtonGhosted,
|
||||||
ontimeButtonGhostedWhite,
|
ontimeButtonGhostedWhite,
|
||||||
ontimeButtonOutlined,
|
ontimeButtonOutlined,
|
||||||
ontimeButtonSubtle,
|
ontimeButtonSubtle,
|
||||||
ontimeButtonSubtleOnLight,
|
|
||||||
ontimeButtonSubtleWhite,
|
ontimeButtonSubtleWhite,
|
||||||
ontimeGhostOnLight,
|
|
||||||
} from './ontimeButton';
|
} from './ontimeButton';
|
||||||
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
|
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
|
||||||
|
import { ontimeDrawer } from './ontimeDrawer';
|
||||||
import { ontimeEditable } from './ontimeEditable';
|
import { ontimeEditable } from './ontimeEditable';
|
||||||
import { ontimeMenuOnDark } from './ontimeMenu';
|
import { ontimeMenuOnDark } from './ontimeMenu';
|
||||||
import { ontimeModal, ontimeSmallModal, ontimeUploadModal } from './ontimeModal';
|
import { ontimeModal } from './ontimeModal';
|
||||||
import { ontimeProgressGray } from './OntimeProgress';
|
|
||||||
import { ontimeBlockRadio } from './ontimeRadio';
|
import { ontimeBlockRadio } from './ontimeRadio';
|
||||||
import { ontimeSelect } from './ontimeSelect';
|
import { ontimeSelect } from './ontimeSelect';
|
||||||
import { lightSwitch, ontimeSwitch } from './ontimeSwitch';
|
import { ontimeSwitch } from './ontimeSwitch';
|
||||||
import { ontimeTab } from './ontimeTab';
|
import { ontimeTab } from './ontimeTab';
|
||||||
import {
|
import {
|
||||||
ontimeInputFilled,
|
ontimeInputFilled,
|
||||||
ontimeInputFilledOnLight,
|
|
||||||
ontimeInputGhosted,
|
ontimeInputGhosted,
|
||||||
ontimeTextAreaFilled,
|
ontimeTextAreaFilled,
|
||||||
ontimeTextAreaFilledOnLight,
|
|
||||||
ontimeTextAreaTransparent,
|
ontimeTextAreaTransparent,
|
||||||
} from './ontimeTextInputs';
|
} from './ontimeTextInputs';
|
||||||
import { ontimeTooltip } from './ontimeTooltip';
|
import { ontimeTooltip } from './ontimeTooltip';
|
||||||
@@ -36,7 +32,6 @@ const theme = extendTheme({
|
|||||||
components: {
|
components: {
|
||||||
Alert: {
|
Alert: {
|
||||||
variants: {
|
variants: {
|
||||||
'ontime-on-light-info': { ...ontimeAlertOnLight },
|
|
||||||
'ontime-on-dark-info': { ...ontimeAlertOnDark },
|
'ontime-on-dark-info': { ...ontimeAlertOnDark },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -53,8 +48,6 @@ const theme = extendTheme({
|
|||||||
'ontime-ghosted': { ...ontimeButtonGhosted },
|
'ontime-ghosted': { ...ontimeButtonGhosted },
|
||||||
'ontime-ghosted-white': { ...ontimeButtonGhostedWhite },
|
'ontime-ghosted-white': { ...ontimeButtonGhostedWhite },
|
||||||
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
|
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
|
||||||
'ontime-subtle-on-light': { ...ontimeButtonSubtleOnLight },
|
|
||||||
'ontime-ghost-on-light': { ...ontimeGhostOnLight },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Checkbox: {
|
Checkbox: {
|
||||||
@@ -62,6 +55,11 @@ const theme = extendTheme({
|
|||||||
'ontime-ondark': { ...ontimeCheckboxOnDark },
|
'ontime-ondark': { ...ontimeCheckboxOnDark },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Drawer: {
|
||||||
|
variants: {
|
||||||
|
'ontime': {...ontimeDrawer},
|
||||||
|
}
|
||||||
|
},
|
||||||
Editable: {
|
Editable: {
|
||||||
variants: {
|
variants: {
|
||||||
ontime: { ...ontimeEditable },
|
ontime: { ...ontimeEditable },
|
||||||
@@ -75,7 +73,6 @@ const theme = extendTheme({
|
|||||||
variants: {
|
variants: {
|
||||||
'ontime-filled': { ...ontimeInputFilled },
|
'ontime-filled': { ...ontimeInputFilled },
|
||||||
'ontime-ghosted': { ...ontimeInputGhosted },
|
'ontime-ghosted': { ...ontimeInputGhosted },
|
||||||
'ontime-filled-on-light': { ...ontimeInputFilledOnLight },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
@@ -84,15 +81,11 @@ const theme = extendTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Modal: {
|
Modal: {
|
||||||
|
baseStyle: {
|
||||||
|
background: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
},
|
||||||
variants: {
|
variants: {
|
||||||
ontime: { ...ontimeModal },
|
ontime: { ...ontimeModal },
|
||||||
'ontime-small': { ...ontimeSmallModal },
|
|
||||||
'ontime-upload': { ...ontimeUploadModal },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Progress: {
|
|
||||||
variants: {
|
|
||||||
'ontime-on-light': { ...ontimeProgressGray },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Radio: {
|
Radio: {
|
||||||
@@ -112,7 +105,6 @@ const theme = extendTheme({
|
|||||||
variants: {
|
variants: {
|
||||||
'ontime-filled': { ...ontimeTextAreaFilled },
|
'ontime-filled': { ...ontimeTextAreaFilled },
|
||||||
'ontime-transparent': { ...ontimeTextAreaTransparent },
|
'ontime-transparent': { ...ontimeTextAreaTransparent },
|
||||||
'ontime-filled-on-light': { ...ontimeTextAreaFilledOnLight },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Tooltip: {
|
Tooltip: {
|
||||||
@@ -121,7 +113,6 @@ const theme = extendTheme({
|
|||||||
Switch: {
|
Switch: {
|
||||||
variants: {
|
variants: {
|
||||||
ontime: { ...ontimeSwitch },
|
ontime: { ...ontimeSwitch },
|
||||||
'ontime-on-light': { ...lightSwitch },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Select: {
|
Select: {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ test('test project file upload', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Rundown menu' }).click();
|
await page.getByRole('button', { name: 'Rundown menu' }).click();
|
||||||
await page.getByRole('menuitem', { name: 'Delete all events' }).click();
|
await page.getByRole('menuitem', { name: 'Delete all events' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Application settings' }).click();
|
await page.getByRole('button', { name: 'toggle settings' }).click();
|
||||||
await page.getByRole('button', { name: 'Project', exact: true }).click();
|
await page.getByRole('button', { name: 'Project', exact: true }).click();
|
||||||
|
|
||||||
// workaround to upload file on hidden input
|
// workaround to upload file on hidden input
|
||||||
|
|||||||
@@ -7,50 +7,50 @@ test.describe('test view navigation feature', () => {
|
|||||||
await page.locator('data-test-id=timer-view');
|
await page.locator('data-test-id=timer-view');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Clock', exact: true }).click();
|
await page.getByRole('link', { name: 'Clock', exact: true }).click();
|
||||||
await page.locator('data-test-id=clock-view');
|
await page.locator('data-test-id=clock-view');
|
||||||
await expect(page).toHaveURL('http://localhost:4001/clock');
|
await expect(page).toHaveURL('http://localhost:4001/clock');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Minimal Timer' }).click();
|
await page.getByRole('link', { name: 'Minimal Timer' }).click();
|
||||||
await page.locator('data-test-id=minimal-timer');
|
await page.locator('data-test-id=minimal-timer');
|
||||||
await expect(page).toHaveURL('http://localhost:4001/minimal');
|
await expect(page).toHaveURL('http://localhost:4001/minimal');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Backstage' }).click();
|
await page.getByRole('link', { name: 'Backstage' }).click();
|
||||||
await page.locator('data-test-id=backstage-view');
|
await page.locator('data-test-id=backstage-view');
|
||||||
await expect(page).toHaveURL('http://localhost:4001/backstage');
|
await expect(page).toHaveURL('http://localhost:4001/backstage');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Public' }).click();
|
await page.getByRole('link', { name: 'Public' }).click();
|
||||||
await page.locator('data-test-id=public-view');
|
await page.locator('data-test-id=public-view');
|
||||||
await expect(page).toHaveURL('http://localhost:4001/public');
|
await expect(page).toHaveURL('http://localhost:4001/public');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Lower Thirds' }).click();
|
await page.getByRole('link', { name: 'Lower Thirds' }).click();
|
||||||
await expect(page).toHaveURL('http://localhost:4001/lower');
|
await expect(page).toHaveURL('http://localhost:4001/lower');
|
||||||
const errorBoundary = await page.locator('data-test-id=error-container');
|
const errorBoundary = await page.locator('data-test-id=error-container');
|
||||||
await expect(errorBoundary).toHaveCount(0);
|
await expect(errorBoundary).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Studio Clock' }).click();
|
await page.getByRole('link', { name: 'Studio Clock' }).click();
|
||||||
await page.locator('data-test-id=studio-view');
|
await page.locator('data-test-id=studio-view');
|
||||||
await expect(page).toHaveURL('http://localhost:4001/studio');
|
await expect(page).toHaveURL('http://localhost:4001/studio');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Countdown' }).click();
|
await page.getByRole('link', { name: 'Countdown' }).click();
|
||||||
await page.locator('data-test-id=countdown-view');
|
await page.locator('data-test-id=countdown-view');
|
||||||
await expect(page).toHaveURL('http://localhost:4001/countdown');
|
await expect(page).toHaveURL('http://localhost:4001/countdown');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||||
await page.locator('data-test-id=navigation-menu');
|
await page.locator('data-test-id=navigation__menu');
|
||||||
await page.getByRole('link', { name: 'Timer', exact: true }).click();
|
await page.getByRole('link', { name: 'Timer', exact: true }).click();
|
||||||
await page.locator('data-test-id=timer-view');
|
await page.locator('data-test-id=timer-view');
|
||||||
await expect(page).toHaveURL('http://localhost:4001/timer');
|
await expect(page).toHaveURL('http://localhost:4001/timer');
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
test('cuesheet displays events and exports csv', async ({ page }) => {
|
test('cuesheet displays events and exports csv', async ({ page }) => {
|
||||||
// same elements in cuesheet
|
// same elements in cuesheet
|
||||||
@@ -9,35 +8,4 @@ test('cuesheet displays events and exports csv', async ({ page }) => {
|
|||||||
await page.getByRole('cell', { name: 'Albania' }).click();
|
await page.getByRole('cell', { name: 'Albania' }).click();
|
||||||
await page.getByRole('cell', { name: 'Latvia' }).click();
|
await page.getByRole('cell', { name: 'Latvia' }).click();
|
||||||
await page.getByRole('cell', { name: 'Lithuania' }).click();
|
await page.getByRole('cell', { name: 'Lithuania' }).click();
|
||||||
|
|
||||||
// From here we test the CSV download feature
|
|
||||||
const downloadPromise = page.waitForEvent('download');
|
|
||||||
await page.getByTestId('cuesheet').getByText('Export CSV').click();
|
|
||||||
|
|
||||||
function validateCSV(contents: string) {
|
|
||||||
// We should try to keep this in sync with the implementation over at cuesheetUtils.ts
|
|
||||||
const expectedHeader = ['Project title: Eurovision Song Contest', 'Project description: Turin 2022'];
|
|
||||||
const expectedColumns = [
|
|
||||||
'Time Start',
|
|
||||||
'Time End',
|
|
||||||
'Duration',
|
|
||||||
'ID',
|
|
||||||
'Colour',
|
|
||||||
'Cue',
|
|
||||||
'Title',
|
|
||||||
'Note',
|
|
||||||
'Is Public? (x)',
|
|
||||||
'Skip?',
|
|
||||||
];
|
|
||||||
const expectedValues = ['Albania', 'Latvia', 'Lithuania', 'Lunch break'];
|
|
||||||
|
|
||||||
const allExpected = [...expectedHeader, ...expectedColumns, ...expectedValues];
|
|
||||||
return allExpected.every((value) => {
|
|
||||||
return contents.toLowerCase().includes(value.toLowerCase());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const download = await downloadPromise;
|
|
||||||
const contents = await fs.promises.readFile(await download.path(), 'utf-8');
|
|
||||||
expect(validateCSV(contents)).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ test('test URL preset feature, it should redirect to given URL', async ({ page }
|
|||||||
await page.goto('http://localhost:4001/editor');
|
await page.goto('http://localhost:4001/editor');
|
||||||
|
|
||||||
// open settings
|
// open settings
|
||||||
await page.getByRole('button', { name: 'Application settings' }).click();
|
await page.getByTestId('navigation__toggle-settings').click();
|
||||||
await page.getByRole('button', { name: 'General' }).click();
|
await page.getByRole('button', { name: 'General' }).click();
|
||||||
|
|
||||||
// create preset
|
// create preset
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ test('sheet file upload', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Rundown menu' }).click();
|
await page.getByRole('button', { name: 'Rundown menu' }).click();
|
||||||
await page.getByRole('menuitem', { name: 'Delete all events' }).click();
|
await page.getByRole('menuitem', { name: 'Delete all events' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Application settings' }).click();
|
await page.getByTestId('navigation__toggle-settings').click();
|
||||||
await page.getByRole('button', { name: 'Import spreadsheet' }).click();
|
await page.getByRole('button', { name: 'Import spreadsheet' }).click();
|
||||||
|
|
||||||
// workaround to upload file on hidden input
|
// workaround to upload file on hidden input
|
||||||
Reference in New Issue
Block a user