mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 08:23:55 +00:00
Navigation (#850)
This commit is contained in:
@@ -49,12 +49,12 @@ export default function AppRouter() {
|
||||
<Route path='/backstage' element={<SBackstage />} />
|
||||
|
||||
<Route path='/public' element={<SPublic />} />
|
||||
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
{/*/!* Lower cannot have fallback *!/*/}
|
||||
|
||||
<Route path='/lower' element={<SLowerThird />} />
|
||||
|
||||
<Route path='/op' element={<Operator />} />
|
||||
<Route path='/operator' element={<Operator />} />
|
||||
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route path='/editor' element={<Editor />} />
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import {
|
||||
DatabaseModel,
|
||||
GetInfo,
|
||||
MessageResponse,
|
||||
ProjectData,
|
||||
ProjectFileListResponse,
|
||||
} from 'ontime-types';
|
||||
import { DatabaseModel, GetInfo, MessageResponse, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
||||
|
||||
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
@use '../../../theme/mixins' as *;
|
||||
|
||||
$menu-bg: $gray-1200;
|
||||
$menu-hover-bg: $gray-1350;
|
||||
$menu-focus-bg: $gray-1300;
|
||||
|
||||
$icon-color: $ui-white;
|
||||
$button-bg: $gray-1050;
|
||||
$button-size: 48px;
|
||||
$button-size: 3rem;
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
@@ -21,7 +20,7 @@ $button-size: 48px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
z-index: 12;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
@@ -29,7 +28,7 @@ $button-size: 48px;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 24px;
|
||||
font-size: 1.5rem;
|
||||
color: $icon-color;
|
||||
background-color: $button-bg;
|
||||
width: $button-size;
|
||||
@@ -44,31 +43,10 @@ $button-size: 48px;
|
||||
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 {
|
||||
@include action-link;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
|
||||
&:hover {
|
||||
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 { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useFullscreen } from '@mantine/hooks';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
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 { IoPencilSharp } from '@react-icons/all-files/io5/IoPencilSharp';
|
||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import useClickOutside from '../../hooks/useClickOutside';
|
||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||
import { isKeyEnter } from '../../utils/keyEvent';
|
||||
|
||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
||||
import { debounce } from '../../utils/debounce';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
function NavigationMenu() {
|
||||
const location = useLocation();
|
||||
interface NavigationMenuProps {
|
||||
editCallback: () => void;
|
||||
}
|
||||
|
||||
function NavigationMenu(props: PropsWithChildren<NavigationMenuProps>) {
|
||||
const { children, editCallback } = props;
|
||||
|
||||
const { fullscreen, toggle } = useFullscreen();
|
||||
const { toggleMirror } = useViewOptionsStore();
|
||||
const [showButton, setShowButton] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useClickOutside(menuRef, () => setShowMenu(false));
|
||||
useClickOutside(menuRef, () => onClose);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
||||
const toggleMenu = () => (isOpen ? onClose() : onOpen());
|
||||
|
||||
// show on mouse move
|
||||
useEffect(() => {
|
||||
@@ -45,98 +42,49 @@ function NavigationMenu() {
|
||||
}
|
||||
fadeOut = setTimeout(() => setShowButton(false), 3000);
|
||||
};
|
||||
document.addEventListener('mousemove', setShowMenuTrue);
|
||||
|
||||
const debouncedShowMenu = debounce(setShowMenuTrue, 1000);
|
||||
|
||||
document.addEventListener('mousemove', debouncedShowMenu);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', setShowMenuTrue);
|
||||
document.removeEventListener('mousemove', debouncedShowMenu);
|
||||
if (fadeOut) {
|
||||
clearTimeout(fadeOut);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFullscreen = () => toggle();
|
||||
const handleMirror = () => toggleMirror();
|
||||
|
||||
const showEditFormDrawer = () => {
|
||||
searchParams.set('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div id='navigation-menu-portal' ref={menuRef}>
|
||||
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
||||
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
||||
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
||||
<div className={`${style.buttonContainer} ${!showButton && !isOpen ? style.hidden : ''}`}>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
aria-label='toggle menu'
|
||||
className={style.navButton}
|
||||
data-testid='navigation__toggle-menu'
|
||||
>
|
||||
<IoApps />
|
||||
</button>
|
||||
<button className={style.button} onClick={showEditFormDrawer}>
|
||||
<IoPencilSharp />
|
||||
<button
|
||||
className={style.button}
|
||||
onClick={editCallback}
|
||||
aria-label='toggle settings'
|
||||
data-testid='navigation__toggle-settings'
|
||||
>
|
||||
<IoSettingsOutline />
|
||||
</button>
|
||||
{showMenu && (
|
||||
<div className={style.menuContainer} data-testid='navigation-menu'>
|
||||
<div className={style.buttonsContainer}>
|
||||
<div
|
||||
className={style.link}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={handleFullscreen}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleFullscreen();
|
||||
}}
|
||||
>
|
||||
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>
|
||||
)}
|
||||
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerCloseButton size='lg' />
|
||||
Ontime
|
||||
</DrawerHeader>
|
||||
<DrawerBody padding={0}>{children}</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
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 { useSocketClientName } from '../../../stores/connectionName';
|
||||
|
||||
import style from './RenameClientModal.module.scss';
|
||||
|
||||
interface RenameClientModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -45,18 +43,18 @@ export default function RenameClientModal({ isOpen, onClose }: RenameClientModal
|
||||
motionPreset='slideInBottom'
|
||||
scrollBehavior='inside'
|
||||
preserveScrollBarGap
|
||||
variant='ontime-small'
|
||||
variant='ontime'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Rename client</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody className={style.modalBody}>
|
||||
<ModalBody>
|
||||
<Input
|
||||
placeholder='Connection must have a name'
|
||||
defaultValue={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
variant='ontime-filled-on-light'
|
||||
variant='ontime-filled'
|
||||
/>
|
||||
<Button
|
||||
isDisabled={newName === clientName || !newName}
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
.drawerContent {
|
||||
background-color: $gray-1250;
|
||||
}
|
||||
|
||||
.drawerHeader {
|
||||
@extend .drawerContent;
|
||||
color: $section-white;
|
||||
}
|
||||
|
||||
.drawerFooter {
|
||||
@extend .drawerContent;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: $section-spacing;
|
||||
|
||||
@@ -72,15 +72,15 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} size='lg'>
|
||||
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} variant='ontime' size='lg'>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerHeader className={style.drawerHeader}>
|
||||
<DrawerCloseButton _hover={{ bg: '#ebedf0', color: '#333' }} size='lg' />
|
||||
<DrawerHeader>
|
||||
<DrawerCloseButton size='lg' />
|
||||
Customise
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody className={style.drawerContent}>
|
||||
<DrawerBody>
|
||||
<form id='edit-params-form' onSubmit={onParamsFormSubmit}>
|
||||
{paramFields.map((field) => (
|
||||
<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 ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
||||
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
||||
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||
import PanelContent from './panel-content/PanelContent';
|
||||
import PanelList from './panel-list/PanelList';
|
||||
@@ -33,6 +34,7 @@ export default function AppSettings() {
|
||||
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
||||
{panel === 'about' && <AboutPanel />}
|
||||
{panel === 'log' && <LogPanel />}
|
||||
{panel === 'shutdown' && <ShutdownPanel />}
|
||||
</PanelContent>
|
||||
</ErrorBoundary>
|
||||
</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',
|
||||
split: true,
|
||||
},
|
||||
{
|
||||
id: 'shutdown',
|
||||
label: 'Shutdown',
|
||||
split: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
||||
|
||||
@@ -4,18 +4,11 @@
|
||||
padding: 1rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-template-rows: 3rem auto 1fr;
|
||||
grid-template-areas:
|
||||
'header'
|
||||
'overview'
|
||||
'settings'
|
||||
'table';
|
||||
gap: 1rem;
|
||||
|
||||
background-color: $gray-1300;
|
||||
color: white;
|
||||
|
||||
& > * {
|
||||
border: 1px solid $white-10;
|
||||
border-radius: 3px;
|
||||
}
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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 { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useCuesheet } from '../../common/hooks/useSocket';
|
||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||
import Overview from '../overview/Overview';
|
||||
|
||||
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
|
||||
import { useCuesheetSettings } from './store/CuesheetSettings';
|
||||
import Cuesheet from './Cuesheet';
|
||||
import { makeCuesheetColumns } from './cuesheetCols';
|
||||
import { makeCSV, makeTable } from './cuesheetUtils';
|
||||
|
||||
import styles from './CuesheetWrapper.module.scss';
|
||||
|
||||
@@ -23,6 +24,7 @@ export default function CuesheetWrapper() {
|
||||
const { updateCustomField } = useEventAction();
|
||||
const featureData = useCuesheet();
|
||||
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
||||
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -74,39 +76,15 @@ export default function CuesheetWrapper() {
|
||||
[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') {
|
||||
return <Empty text='Loading...' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||
<CuesheetTableHeader handleExport={exportHandler} featureData={featureData} />
|
||||
<Overview />
|
||||
<CuesheetProgress />
|
||||
<ProductionNavigationMenu handleSettings={() => toggleSettings()} />
|
||||
<Cuesheet
|
||||
data={flatRundown}
|
||||
columns={columns}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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 { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
import { Playback, ProjectData } from 'ontime-types';
|
||||
|
||||
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
||||
@@ -28,10 +24,7 @@ interface CuesheetTableHeaderProps {
|
||||
|
||||
export default function CuesheetTableHeader({ handleExport, featureData }: CuesheetTableHeaderProps) {
|
||||
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 { fullscreen, toggle } = useFullscreen();
|
||||
const { data: project } = useProjectData();
|
||||
|
||||
const exportProject = () => {
|
||||
@@ -66,19 +59,6 @@ export default function CuesheetTableHeader({ handleExport, featureData }: Cuesh
|
||||
<IoLocate />
|
||||
</span>
|
||||
</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'>
|
||||
<span className={style.actionText} onClick={exportProject}>
|
||||
Export CSV
|
||||
|
||||
@@ -23,12 +23,14 @@ interface CuesheetTableSettingsProps {
|
||||
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
||||
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
|
||||
const {
|
||||
followSelected,
|
||||
toggleFollow,
|
||||
showPrevious,
|
||||
toggleDelayVisibility,
|
||||
togglePreviousVisibility,
|
||||
showDelayBlock,
|
||||
showDelayedTimes,
|
||||
toggleDelayedTimes,
|
||||
togglePreviousVisibility,
|
||||
toggleDelayVisibility,
|
||||
} = useCuesheetSettings();
|
||||
|
||||
return (
|
||||
@@ -53,6 +55,10 @@ function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
||||
</div>
|
||||
<div className={style.sectionTitle}>Table Options</div>
|
||||
<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}>
|
||||
<Switch variant='ontime' size='sm' isChecked={showPrevious} onChange={() => togglePreviousVisibility()} />
|
||||
Show past events
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@use './EditorMixin' as editor;
|
||||
|
||||
$menu-width: 2.75rem;
|
||||
$min-playback-width: 27rem;
|
||||
$max-playback-width: 35rem;
|
||||
$panel-gap: 0.5rem;
|
||||
@@ -17,11 +16,11 @@ $panel-gap: 0.5rem;
|
||||
padding: 1rem 0.5rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: $menu-width auto;
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: 3rem 1fr;
|
||||
grid-template-areas:
|
||||
'menu overview'
|
||||
'menu main';
|
||||
'overview'
|
||||
'main';
|
||||
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 { SettingsOptionId } from '../app-settings/settingsStore';
|
||||
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import Overview from '../overview/Overview';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
@@ -15,14 +14,46 @@ const MessageControl = lazy(() => import('../control/message/MessageControlExpor
|
||||
|
||||
export default function Editor() {
|
||||
const { isOpen, setLocation, close } = useAppSettingsNavigation();
|
||||
const { isElectron } = useElectronEvent();
|
||||
|
||||
const handleSettings = (newTab?: SettingsOptionId) => {
|
||||
const handleSettings = useCallback(() => {
|
||||
if (isOpen) {
|
||||
close();
|
||||
} 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
|
||||
useEffect(() => {
|
||||
@@ -31,9 +62,7 @@ export default function Editor() {
|
||||
|
||||
return (
|
||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||
<ErrorBoundary>
|
||||
<MenuBar openSettings={handleSettings} isSettingsOpen={isOpen} />
|
||||
</ErrorBoundary>
|
||||
<ProductionNavigationMenu handleSettings={handleSettings} />
|
||||
{isOpen ? (
|
||||
<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 { 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 { getOperatorOptions } from '../../common/components/view-params-editor/constants';
|
||||
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 featureData = useOperator();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
const [showEditPrompt, setShowEditPrompt] = useState(false);
|
||||
@@ -116,6 +116,11 @@ export default function Operator() {
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const showEditFormDrawer = useCallback(() => {
|
||||
searchParams.set('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
const missingData = !data || !customFields || !projectData;
|
||||
const isLoading = status === 'pending' || customFieldStatus === 'pending' || projectDataStatus === 'pending';
|
||||
|
||||
@@ -141,7 +146,7 @@ export default function Operator() {
|
||||
|
||||
return (
|
||||
<div className={style.operatorContainer}>
|
||||
<NavigationMenu />
|
||||
<ProductionNavigationMenu handleSettings={showEditFormDrawer} />
|
||||
<ViewParamsEditor paramFields={operatorOptions} />
|
||||
{editEvent && <EditModal event={editEvent} onClose={() => setEditEvent(null)} />}
|
||||
|
||||
|
||||
@@ -1,38 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { dayInMs, millisToString } from 'ontime-utils';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import { useRuntimeOverview, useRuntimePlaybackOverview } from '../../common/hooks/useSocket';
|
||||
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 { calculateEndAndDaySpan, formatedTime, getOffsetText } from './overviewUtils';
|
||||
|
||||
import style from './Overview.module.scss';
|
||||
|
||||
/**
|
||||
* Encapsulates the logic for formatting time in overview
|
||||
* @param time
|
||||
* @returns
|
||||
*/
|
||||
function formatedTime(time: MaybeNumber) {
|
||||
return millisToString(time, { fallback: timerPlaceholder });
|
||||
}
|
||||
export default memo(Overview);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
export default function Overview() {
|
||||
function Overview() {
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||
|
||||
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() {
|
||||
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';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||
@@ -93,29 +94,32 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
backstageEvents={rundownData}
|
||||
customFields={customFields}
|
||||
eventNext={eventNext}
|
||||
eventNow={eventNow}
|
||||
events={publicEvents}
|
||||
external={message.external}
|
||||
general={project}
|
||||
isMirrored={isMirrored}
|
||||
lower={message.lower}
|
||||
nextId={nextId}
|
||||
onAir={onAir}
|
||||
pres={message.timer}
|
||||
publ={message.public}
|
||||
publicEventNext={publicEventNext}
|
||||
publicEventNow={publicEventNow}
|
||||
publicSelectedId={publicSelectedId}
|
||||
selectedId={selectedId}
|
||||
settings={settings}
|
||||
time={TimeManagerType}
|
||||
viewSettings={viewSettings}
|
||||
/>
|
||||
<>
|
||||
<ViewNavigationMenu />
|
||||
<Component
|
||||
{...props}
|
||||
backstageEvents={rundownData}
|
||||
customFields={customFields}
|
||||
eventNext={eventNext}
|
||||
eventNow={eventNow}
|
||||
events={publicEvents}
|
||||
external={message.external}
|
||||
general={project}
|
||||
isMirrored={isMirrored}
|
||||
lower={message.lower}
|
||||
nextId={nextId}
|
||||
onAir={onAir}
|
||||
pres={message.timer}
|
||||
publ={message.public}
|
||||
publicEventNext={publicEventNext}
|
||||
publicEventNow={publicEventNow}
|
||||
publicSelectedId={publicSelectedId}
|
||||
selectedId={selectedId}
|
||||
settings={settings}
|
||||
time={TimeManagerType}
|
||||
viewSettings={viewSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { CustomFields, Message, OntimeEvent, ProjectData, Settings, SupportedEve
|
||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/constants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||
import Schedule from '../../../common/components/schedule/Schedule';
|
||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||
@@ -102,7 +101,6 @@ export default function Backstage(props: BackstageProps) {
|
||||
|
||||
return (
|
||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={backstageOptions} />
|
||||
<div className='project-header'>
|
||||
{general.title}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { Settings, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/constants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -133,7 +132,6 @@ export default function Clock(props: ClockProps) {
|
||||
}}
|
||||
data-testid='clock-view'
|
||||
>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={clockOptions} />
|
||||
<SuperscriptTime
|
||||
time={clock}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, Vi
|
||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/constants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -110,7 +109,6 @@ export default function Countdown(props: CountdownProps) {
|
||||
|
||||
return (
|
||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={timeOption} />
|
||||
{follow === null ? (
|
||||
<CountdownSelect events={backstageEvents} />
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { CustomFields, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/constants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -191,7 +190,6 @@ export default function LowerThird(props: LowerProps) {
|
||||
|
||||
return (
|
||||
<div className='lower-third' style={{ backgroundColor: `#${options.key}` }}>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={getLowerThirdOptions(customFields)} />
|
||||
<div
|
||||
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 { 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 ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -186,7 +185,6 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
}}
|
||||
data-testid='minimal-timer'
|
||||
>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={MINIMAL_TIMER_OPTIONS} />
|
||||
{!hideMessagesOverlay && (
|
||||
<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 { overrideStylesURL } from '../../../common/api/constants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import Schedule from '../../../common/components/schedule/Schedule';
|
||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||
@@ -78,7 +77,6 @@ export default function Public(props: BackstageProps) {
|
||||
|
||||
return (
|
||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={publicOptions} />
|
||||
<div className='project-header'>
|
||||
{general.title}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { isOntimeEvent, Playback } from 'ontime-types';
|
||||
import { millisToString, removeSeconds } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/constants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import useFitText from '../../../common/hooks/useFitText';
|
||||
@@ -78,7 +77,6 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
|
||||
return (
|
||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||
<div className='clock-container'>
|
||||
{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 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 { getTimerOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -162,7 +161,6 @@ export default function Timer(props: TimerProps) {
|
||||
|
||||
return (
|
||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={timerOptions} />
|
||||
<div className={showBlackout ? 'blackout blackout--active' : 'blackout'} />
|
||||
{!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 = {
|
||||
container: {
|
||||
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 = {
|
||||
...ontimeButtonSubtle,
|
||||
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',
|
||||
padding: '1rem 1.5rem',
|
||||
fontSize: '1.25rem',
|
||||
color: '#202020', // $gray-50
|
||||
color: '#fefefe', // $gray-50
|
||||
},
|
||||
dialog: {
|
||||
borderRadius: '3px',
|
||||
padding: 0,
|
||||
minHeight: 'min(500px, 75vh)',
|
||||
minHeight: 'min(200px, 10vh)',
|
||||
backgroundColor: '#202020', // $gray-1250
|
||||
color: '#fefefe', // $gray-50
|
||||
},
|
||||
body: {
|
||||
padding: 0,
|
||||
padding: '1rem',
|
||||
fontSize: 'calc(1rem - 2px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
},
|
||||
closeButton: {
|
||||
color: '#202020', // $gray-50
|
||||
color: '#fefefe', // $gray-50
|
||||
},
|
||||
footer: {
|
||||
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 = {
|
||||
...commonStyles,
|
||||
};
|
||||
|
||||
export const ontimeTextAreaTransparent = {
|
||||
...commonStyles,
|
||||
backgroundColor: 'transparent',
|
||||
@@ -66,24 +49,3 @@ export const ontimeTextAreaTransparent = {
|
||||
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 { ontimeAlertOnDark, ontimeAlertOnLight } from './OntimeAlert';
|
||||
import { ontimeAlertOnDark } from './OntimeAlert';
|
||||
import {
|
||||
ontimeButtonFilled,
|
||||
ontimeButtonGhosted,
|
||||
ontimeButtonGhostedWhite,
|
||||
ontimeButtonOutlined,
|
||||
ontimeButtonSubtle,
|
||||
ontimeButtonSubtleOnLight,
|
||||
ontimeButtonSubtleWhite,
|
||||
ontimeGhostOnLight,
|
||||
} from './ontimeButton';
|
||||
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
|
||||
import { ontimeDrawer } from './ontimeDrawer';
|
||||
import { ontimeEditable } from './ontimeEditable';
|
||||
import { ontimeMenuOnDark } from './ontimeMenu';
|
||||
import { ontimeModal, ontimeSmallModal, ontimeUploadModal } from './ontimeModal';
|
||||
import { ontimeProgressGray } from './OntimeProgress';
|
||||
import { ontimeModal } from './ontimeModal';
|
||||
import { ontimeBlockRadio } from './ontimeRadio';
|
||||
import { ontimeSelect } from './ontimeSelect';
|
||||
import { lightSwitch, ontimeSwitch } from './ontimeSwitch';
|
||||
import { ontimeSwitch } from './ontimeSwitch';
|
||||
import { ontimeTab } from './ontimeTab';
|
||||
import {
|
||||
ontimeInputFilled,
|
||||
ontimeInputFilledOnLight,
|
||||
ontimeInputGhosted,
|
||||
ontimeTextAreaFilled,
|
||||
ontimeTextAreaFilledOnLight,
|
||||
ontimeTextAreaTransparent,
|
||||
} from './ontimeTextInputs';
|
||||
import { ontimeTooltip } from './ontimeTooltip';
|
||||
@@ -36,7 +32,6 @@ const theme = extendTheme({
|
||||
components: {
|
||||
Alert: {
|
||||
variants: {
|
||||
'ontime-on-light-info': { ...ontimeAlertOnLight },
|
||||
'ontime-on-dark-info': { ...ontimeAlertOnDark },
|
||||
},
|
||||
},
|
||||
@@ -53,8 +48,6 @@ const theme = extendTheme({
|
||||
'ontime-ghosted': { ...ontimeButtonGhosted },
|
||||
'ontime-ghosted-white': { ...ontimeButtonGhostedWhite },
|
||||
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
|
||||
'ontime-subtle-on-light': { ...ontimeButtonSubtleOnLight },
|
||||
'ontime-ghost-on-light': { ...ontimeGhostOnLight },
|
||||
},
|
||||
},
|
||||
Checkbox: {
|
||||
@@ -62,6 +55,11 @@ const theme = extendTheme({
|
||||
'ontime-ondark': { ...ontimeCheckboxOnDark },
|
||||
},
|
||||
},
|
||||
Drawer: {
|
||||
variants: {
|
||||
'ontime': {...ontimeDrawer},
|
||||
}
|
||||
},
|
||||
Editable: {
|
||||
variants: {
|
||||
ontime: { ...ontimeEditable },
|
||||
@@ -75,7 +73,6 @@ const theme = extendTheme({
|
||||
variants: {
|
||||
'ontime-filled': { ...ontimeInputFilled },
|
||||
'ontime-ghosted': { ...ontimeInputGhosted },
|
||||
'ontime-filled-on-light': { ...ontimeInputFilledOnLight },
|
||||
},
|
||||
},
|
||||
Menu: {
|
||||
@@ -84,15 +81,11 @@ const theme = extendTheme({
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
baseStyle: {
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
variants: {
|
||||
ontime: { ...ontimeModal },
|
||||
'ontime-small': { ...ontimeSmallModal },
|
||||
'ontime-upload': { ...ontimeUploadModal },
|
||||
},
|
||||
},
|
||||
Progress: {
|
||||
variants: {
|
||||
'ontime-on-light': { ...ontimeProgressGray },
|
||||
},
|
||||
},
|
||||
Radio: {
|
||||
@@ -112,7 +105,6 @@ const theme = extendTheme({
|
||||
variants: {
|
||||
'ontime-filled': { ...ontimeTextAreaFilled },
|
||||
'ontime-transparent': { ...ontimeTextAreaTransparent },
|
||||
'ontime-filled-on-light': { ...ontimeTextAreaFilledOnLight },
|
||||
},
|
||||
},
|
||||
Tooltip: {
|
||||
@@ -121,7 +113,6 @@ const theme = extendTheme({
|
||||
Switch: {
|
||||
variants: {
|
||||
ontime: { ...ontimeSwitch },
|
||||
'ontime-on-light': { ...lightSwitch },
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
|
||||
@@ -7,7 +7,7 @@ test('test project file upload', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Rundown menu' }).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();
|
||||
|
||||
// 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.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.locator('data-test-id=clock-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/clock');
|
||||
|
||||
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.locator('data-test-id=minimal-timer');
|
||||
await expect(page).toHaveURL('http://localhost:4001/minimal');
|
||||
|
||||
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.locator('data-test-id=backstage-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/backstage');
|
||||
|
||||
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.locator('data-test-id=public-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/public');
|
||||
|
||||
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 expect(page).toHaveURL('http://localhost:4001/lower');
|
||||
const errorBoundary = await page.locator('data-test-id=error-container');
|
||||
await expect(errorBoundary).toHaveCount(0);
|
||||
|
||||
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.locator('data-test-id=studio-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/studio');
|
||||
|
||||
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.locator('data-test-id=countdown-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/countdown');
|
||||
|
||||
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.locator('data-test-id=timer-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/timer');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
test('cuesheet displays events and exports csv', async ({ page }) => {
|
||||
// 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: 'Latvia' }).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');
|
||||
|
||||
// open settings
|
||||
await page.getByRole('button', { name: 'Application settings' }).click();
|
||||
await page.getByTestId('navigation__toggle-settings').click();
|
||||
await page.getByRole('button', { name: 'General' }).click();
|
||||
|
||||
// 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('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();
|
||||
|
||||
// workaround to upload file on hidden input
|
||||
Reference in New Issue
Block a user