Navigation (#850)

This commit is contained in:
Carlos Valente
2024-03-27 21:02:40 +01:00
committed by GitHub
parent f5e9a2549e
commit ce14aabcd7
46 changed files with 476 additions and 614 deletions
+2 -2
View File
@@ -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 -7
View File
@@ -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);
@@ -1,5 +0,0 @@
.modalBody {
display: flex;
flex-direction: column;
gap: 1rem;
}
@@ -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;
} }
+39 -10
View File
@@ -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;
}
-83
View File
@@ -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)} />}
+5 -37
View File
@@ -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 && (
-12
View File
@@ -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)',
-8
View File
@@ -1,8 +0,0 @@
export const ontimeProgressGray = {
track: {
background: '#f6f6f6', // $gray-500
},
filledTrack: {
background: '#578AF4', // $blue-500
},
};
-32
View File
@@ -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
+20
View File
@@ -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
},
},
};
+8 -27
View File
@@ -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)',
},
};
-13
View File
@@ -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
},
},
};
+1 -39
View File
@@ -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',
},
},
};
+12 -21
View File
@@ -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: {
+1 -1
View File
@@ -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
+8 -8
View File
@@ -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 -33
View File
@@ -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);
}); });
+1 -1
View File
@@ -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
@@ -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