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='/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 -7
View File
@@ -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);
@@ -1,5 +0,0 @@
.modalBody {
display: flex;
flex-direction: column;
gap: 1rem;
}
@@ -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;
}
+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 { 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;
}
-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 { 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)} />}
+5 -37
View File
@@ -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 && (
-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 = {
container: {
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 = {
...ontimeButtonSubtle,
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',
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)',
},
};
-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 = {
...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',
},
},
};
+12 -21
View File
@@ -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: {
+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('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
+8 -8
View File
@@ -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 -33
View File
@@ -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);
});
+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');
// 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
@@ -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