Compare commits

..

17 Commits

Author SHA1 Message Date
Bianca Procopio 4d1260321f update multiple events "defaults" 2024-04-08 14:46:28 -04:00
Bianca Procopio 7577f307bf adding "unkown" color to swatch 2024-04-08 09:34:29 -06:00
Bianca Procopio 997be35815 Merge branch 'v3' of https://github.com/cpvalente/ontime into bp/edit-multiple-events 2024-04-07 16:28:13 -06:00
Ary f9b1880e88 Fix to cuesheet table header not resizing (#852) 2024-04-06 17:40:03 +02:00
Alex Christoffer Rasmussen 8a1474e8d6 Improve error handling in server (#812)
* add functions to convert all errors to `ErrorResponse` type

* replace all `{ message: error.toString() }` with `toErrorResponse(error)`

* refactor: all controllers to to use getErrorMessage and add types to the Response

* 404 for nonexistent api routes
2024-04-05 23:06:40 +02:00
Carlos Valente d432f1e3ff Typescript null (#869)
* refactor: improve null checking

* fix: return result of mutation
2024-04-05 12:07:26 +02:00
Ary 9f207dd39a Fix: Removed updatedAt from project list (#853) 2024-04-03 21:35:18 +02:00
Alex Christoffer Rasmussen 8b8b3347fb fix: Sheet import error when user doesn't have correct permissions for the sheet (#865)
* refactor: better throw error message

* fix: await verify so wa catch the potential error

* fix: catch and display error about varification

* fix: don't block the Authenticate with a spinner

* center spinner

* propper boolean
2024-04-03 14:18:04 +02:00
Carlos Valente 01c2ef4c4d V3 feedback (#866)
* refactor: rename settings panel

* style: typo

* style: fix blackout position

* fix: offset time in time-to-end

* refactor: operator is protected

* style: tweaks to pincode

* refactor: close params editor on submit

* refactor: same navigation in all pages

* refactor: use cached response
2024-04-02 22:45:03 +02:00
Carlos Valente 10852eecdd Rundown modes (#864)
* refactor: simplify settings

* refactor: quick add around selection

* refactor: no action menu in delay block

* refactor: no action menu in event block

* style: context menu dark

* refactor: context menu actions
2024-04-02 20:09:27 +02:00
Bianca Procopio e221543dc7 initial setup for edit multiple events 2024-03-31 14:45:08 -06:00
Alex Christoffer Rasmussen 01000f8837 feat: chaching for static react files (#862)
* set immutable tag for static route

* captur more server timings in NODE_ENV === 'development'
2024-03-31 22:07:42 +02:00
Carlos Valente 9a03e4850a Navigation3 (#860) 2024-03-31 21:18:24 +02:00
Alex Christoffer Rasmussen 6bb3fb2931 fix: editor setting path (#859) 2024-03-29 17:09:18 +01:00
Alex Christoffer Rasmussen a86496ed27 fix: customFields was not persisted on import and force Custom Field keys to be lower case (#849)
* refactor: force Custom Field keys to be lower case

* fix: customFields was not persisted on import
2024-03-28 13:51:32 +01:00
Carlos Valente df8b76305e Alpha 3 cleanup (#857) 2024-03-28 13:35:36 +01:00
Carlos Valente ce14aabcd7 Navigation (#850) 2024-03-27 21:02:40 +01:00
149 changed files with 1713 additions and 1617 deletions
+1
View File
@@ -19,6 +19,7 @@
"rules": {
"no-useless-concat": "warn",
"prefer-template": "warn",
"no-throw-literal": "error",
"no-console": [
"warn",
{
+1 -1
View File
@@ -86,7 +86,7 @@
"ontime-utils": "workspace:*",
"prettier": "^3.0.3",
"sass": "^1.57.1",
"typescript": "^5.2.2",
"typescript": "^5.4.3",
"vite": "^5.1.0",
"vite-plugin-compression2": "^0.12.0",
"vite-plugin-svgr": "^4.2.0",
+3 -3
View File
@@ -7,7 +7,7 @@ import withData from './features/viewers/ViewWrapper';
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
const Operator = lazy(() => import('./features/operator/Operator'));
const Operator = lazy(() => import('./features/operator/OperatorExport'));
const TimerView = lazy(() => import('./features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
@@ -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';
@@ -25,5 +25,11 @@ export default function Swatch(props: SwatchProps) {
</div>
);
}
if (color === 'unkown') {
<div className={`${classes} ${style.center}`} onClick={handleClick}>
?
</div>;
}
return <div className={classes} style={{ backgroundColor: `${color}` }} onClick={handleClick} />;
}
@@ -8,10 +8,12 @@ interface ColourInputProps {
value: string;
name: 'colour';
handleChange: (newValue: 'colour', name: string) => void;
isMultiple?: boolean;
}
const colours = [
'',
'unkown',
'#FFCC78', // $orange-400
'#FFAB33', // $orange-600
'#77C785', // $green-400
@@ -27,7 +29,7 @@ const colours = [
];
export default function SwatchSelect(props: ColourInputProps) {
const { value, name, handleChange } = props;
const { value, name, handleChange, isMultiple } = props;
const setColour = useCallback(
(newValue: string) => {
@@ -40,9 +42,15 @@ export default function SwatchSelect(props: ColourInputProps) {
return (
<div className={style.list}>
{colours.map((colour) => (
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
))}
{colours
.filter((colour) => {
// Only include unkown if it is multiple
if (!isMultiple && colour === 'unkown') return false;
return true;
})
.map((colour) => (
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
))}
</div>
);
}
@@ -1,7 +1,7 @@
import { useCallback, useRef } from 'react';
import { Input, Textarea } from '@chakra-ui/react';
import { EventEditorSubmitActions } from '../../../../features/rundown/event-editor/EventEditor';
import { EventEditorSubmitActions } from '../../../../features/rundown/event-editor/EventEditorWrapper';
import { Size } from '../../../models/Util.type';
import useReactiveTextInput from './useReactiveTextInput';
@@ -0,0 +1,60 @@
import { useEffect, useState } from 'react';
import { IoApps } from '@react-icons/all-files/io5/IoApps';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { debounce } from '../../utils/debounce';
import style from './NavigationMenu.module.scss';
interface FloatingNavigationProps {
toggleMenu: () => void;
toggleSettings: () => void;
}
export default function FloatingNavigation(props: FloatingNavigationProps) {
const { toggleMenu, toggleSettings } = props;
const [showButton, setShowButton] = useState(false);
// show on mouse move
useEffect(() => {
let fadeOut: NodeJS.Timeout | null = null;
const setShowMenuTrue = () => {
setShowButton(true);
if (fadeOut) {
clearTimeout(fadeOut);
}
fadeOut = setTimeout(() => setShowButton(false), 3000);
};
const debouncedShowMenu = debounce(setShowMenuTrue, 1000);
document.addEventListener('mousemove', debouncedShowMenu);
return () => {
document.removeEventListener('mousemove', debouncedShowMenu);
if (fadeOut) {
clearTimeout(fadeOut);
}
};
}, []);
return (
<div className={`${style.buttonContainer} ${!showButton ? style.hidden : ''}`}>
<button
onClick={toggleMenu}
aria-label='toggle menu'
className={style.navButton}
data-testid='navigation__toggle-menu'
>
<IoApps />
</button>
<button
className={style.button}
onClick={toggleSettings}
aria-label='toggle settings'
data-testid='navigation__toggle-settings'
>
<IoSettingsOutline />
</button>
</div>
);
}
@@ -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;
gap: 0.5rem;
&:hover {
background-color: $menu-hover-bg;
@@ -91,7 +69,7 @@ $button-size: 48px;
}
.linkIcon {
display: inline-block;
margin-left: auto;
transform: rotate(45deg);
}
@@ -1,13 +1,20 @@
import { memo, useEffect, useRef, useState } from 'react';
import { memo, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { useDisclosure } from '@chakra-ui/react';
import { Link } from 'react-router-dom';
import {
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
DrawerOverlay,
useDisclosure,
} from '@chakra-ui/react';
import { useFullscreen } from '@mantine/hooks';
import { 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 { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { navigatorConstants } from '../../../viewerConfig';
@@ -19,69 +26,42 @@ import RenameClientModal from './rename-client-modal/RenameClientModal';
import style from './NavigationMenu.module.scss';
function NavigationMenu() {
const location = useLocation();
interface NavigationMenuProps {
isOpen: boolean;
onClose: () => void;
}
function NavigationMenu(props: NavigationMenuProps) {
const { isOpen, onClose } = props;
const { isOpen: isRenameOpen, onOpen: onRenameOpen, onClose: onRenameClose } = useDisclosure();
const { fullscreen, toggle } = useFullscreen();
const { toggleMirror } = useViewOptionsStore();
const [showButton, setShowButton] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
useClickOutside(menuRef, () => setShowMenu(false));
const { isOpen, onOpen, onClose } = useDisclosure();
const toggleMenu = () => setShowMenu((prev) => !prev);
// show on mouse move
useEffect(() => {
let fadeOut: NodeJS.Timeout | null = null;
const setShowMenuTrue = () => {
setShowButton(true);
if (fadeOut) {
clearTimeout(fadeOut);
}
fadeOut = setTimeout(() => setShowButton(false), 3000);
};
document.addEventListener('mousemove', setShowMenuTrue);
return () => {
document.removeEventListener('mousemove', setShowMenuTrue);
if (fadeOut) {
clearTimeout(fadeOut);
}
};
}, []);
const handleFullscreen = () => toggle();
const handleMirror = () => toggleMirror();
const showEditFormDrawer = () => {
searchParams.set('edit', 'true');
setSearchParams(searchParams);
};
useClickOutside(menuRef, () => onClose);
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}>
<IoApps />
</button>
<button className={style.button} onClick={showEditFormDrawer}>
<IoPencilSharp />
</button>
{showMenu && (
<div className={style.menuContainer} data-testid='navigation-menu'>
<RenameClientModal isOpen={isRenameOpen} onClose={onRenameClose} />
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
<DrawerOverlay />
<DrawerContent>
<DrawerHeader>
<DrawerCloseButton size='lg' />
Ontime
</DrawerHeader>
<DrawerBody padding={0}>
<div className={style.buttonsContainer}>
<div
className={style.link}
tabIndex={0}
role='button'
onClick={handleFullscreen}
onClick={toggle}
onKeyDown={(event) => {
isKeyEnter(event) && handleFullscreen();
isKeyEnter(event) && toggle();
}}
>
Toggle Fullscreen
@@ -91,9 +71,9 @@ function NavigationMenu() {
className={style.link}
tabIndex={0}
role='button'
onClick={handleMirror}
onClick={() => toggleMirror()}
onKeyDown={(event) => {
isKeyEnter(event) && handleMirror();
isKeyEnter(event) && toggleMirror();
}}
>
Flip Screen
@@ -103,20 +83,35 @@ function NavigationMenu() {
className={style.link}
tabIndex={0}
role='button'
onClick={onOpen}
onClick={onRenameOpen}
onKeyDown={(event) => {
isKeyEnter(event) && onOpen();
isKeyEnter(event) && onRenameOpen();
}}
>
Rename Client
</div>
</div>
<hr className={style.separator} />
<Link to='/cuesheet' className={style.link} tabIndex={0}>
<Link
to='/editor'
className={`${style.link} ${location.pathname === '/editor' ? style.current : ''}`}
tabIndex={0}
>
<IoLockClosedOutline />
Editor
<IoArrowUp className={style.linkIcon} />
</Link>
<Link
to='/cuesheet'
className={`${style.link} ${location.pathname === '/cuesheet' ? style.current : ''}`}
tabIndex={0}
>
<IoLockClosedOutline />
Cuesheet
<IoArrowUp className={style.linkIcon} />
</Link>
<Link to='/op' className={style.link} tabIndex={0}>
<Link to='/op' className={`${style.link} ${location.pathname === '/op' ? style.current : ''}`} tabIndex={0}>
<IoLockClosedOutline />
Operator
<IoArrowUp className={style.linkIcon} />
</Link>
@@ -132,11 +127,10 @@ function NavigationMenu() {
<IoArrowUp className={style.linkIcon} />
</Link>
))}
</div>
)}
</div>
</DrawerBody>
</DrawerContent>
</Drawer>
</div>,
document.body,
);
}
@@ -0,0 +1,16 @@
import { memo } from 'react';
import NavigationMenu from './NavigationMenu';
interface ProductionNavigationMenuProps {
isMenuOpen: boolean;
onMenuClose: () => void;
}
function ProductionNavigationMenu(props: ProductionNavigationMenuProps) {
const { isMenuOpen, onMenuClose } = props;
return <NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />;
}
export default memo(ProductionNavigationMenu);
@@ -0,0 +1,27 @@
import { memo, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useDisclosure } from '@chakra-ui/react';
import FloatingNavigation from './FloatingNavigation';
import NavigationMenu from './NavigationMenu';
function ViewNavigationMenu() {
const [searchParams, setSearchParams] = useSearchParams();
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
const showEditFormDrawer = useCallback(() => {
searchParams.set('edit', 'true');
setSearchParams(searchParams);
}, [searchParams, setSearchParams]);
const toggleMenu = () => (isMenuOpen ? onMenuClose() : onMenuOpen());
return (
<>
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
<NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />
</>
);
}
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,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { HStack, IconButton, PinInput, PinInputField } from '@chakra-ui/react';
import { IconButton, PinInput, PinInputField } from '@chakra-ui/react';
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
import style from './ProtectRoute.module.scss';
@@ -39,8 +39,8 @@ export default function PinPage(props: PinPageProps) {
return (
<div className={style.container}>
{`Ontime ${permission || ''}`}
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
{`Ontime ${permission}`}
<div className={failed ? style.pin__failed : style.pin}>
<PinInput
type='alphanumeric'
size='lg'
@@ -57,8 +57,15 @@ export default function PinPage(props: PinPageProps) {
<PinInputField />
<PinInputField />
</PinInput>
<IconButton aria-label='Enter' size='lg' isRound icon={<IoCheckmark />} onClick={validate} />
</HStack>
<IconButton
variant='ontime-filled'
aria-label='Enter'
size='lg'
isRound
icon={<IoCheckmark />}
onClick={validate}
/>
</div>
</div>
);
}
@@ -1,27 +1,31 @@
.container {
display: grid;
place-content: center;
height: 100vh;
padding-bottom: 30vh;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 25vh;
background: $bg-container-l1;
color: $ontime-color;
font-family: $ontime-font-family;
font-weight: 200;
text-align: center;
font-size: 3vw;
font-size: 3rem;
}
.pin,
.pin__failed {
padding: 20px;
display: flex;
gap: 0.125em;
padding-block: 0.5em;
input {
border-radius: 50%;
border-radius: 99px;
border-color: $gray-500;
&:hover {
border-color: $blue-500;
}
}
button {
margin-left: 20px;
margin-left: 1em;
}
}
@@ -33,9 +37,9 @@
@keyframes colourFade {
from {
background: $action-blue;
background: $red-500;
}
to {
background: rgba($action-blue, 0);
background: rgba($red-500, 0);
}
}
}
@@ -1,24 +1,9 @@
.drawerContent {
background-color: $gray-1250;
}
.drawerHeader {
@extend .drawerContent;
color: $section-white;
}
.drawerFooter {
@extend .drawerContent;
display: flex;
justify-content: start;
justify-content: end;
gap: $section-spacing;
button[type='reset'] {
padding: 0 2em;
margin-right: auto;
}
button[type='submit'] {
button {
padding: 0 2em;
}
}
@@ -26,6 +11,9 @@
.label {
font-size: $inner-section-text-size;
color: $label-gray;
display: flex;
flex-direction: column;
gap: 0.25rem
}
.columnSection {
@@ -52,15 +52,16 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
}
}, [searchParams, onOpen]);
const onCloseWithoutSaving = () => {
onClose();
const handleClose = () => {
searchParams.delete('edit');
setSearchParams(searchParams);
onClose();
};
const resetParams = () => {
setSearchParams();
onClose();
};
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
@@ -69,18 +70,20 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
setSearchParams(newSearchParams);
handleClose();
};
return (
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} size='lg'>
<Drawer isOpen={isOpen} placement='right' onClose={handleClose} 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}>
@@ -96,10 +99,7 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
<DrawerFooter className={style.drawerFooter}>
<Button variant='ontime-ghosted' onClick={resetParams} type='reset'>
Reset
</Button>
<Button variant='ontime-subtle' onClick={onCloseWithoutSaving}>
Cancel
Reset to default
</Button>
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
Save
@@ -11,7 +11,7 @@ export default function useCustomFields() {
const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: CUSTOM_FIELDS,
queryFn: getCustomFields,
placeholderData: placeholder,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchInterval,
@@ -11,14 +11,13 @@ export function useHttpSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: HTTP_SETTINGS,
queryFn: getHTTP,
placeholderData: httpPlaceholder,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt: number) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
});
// we need to jump through some hoops because of the type op port
return { data: data ?? httpPlaceholder, status, isFetching, isError, refetch };
}
@@ -10,7 +10,7 @@ export default function useInfo() {
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
queryKey: APP_INFO,
queryFn: getInfo,
placeholderData: ontimePlaceholderInfo,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
@@ -11,7 +11,7 @@ export default function useOscSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: OSC_SETTINGS,
queryFn: getOSC,
placeholderData: oscPlaceholderSettings,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt: number) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
@@ -9,7 +9,7 @@ export default function useProjectData() {
const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: PROJECT_DATA,
queryFn: getProjectData,
placeholderData: projectDataPlaceholder,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
@@ -14,7 +14,7 @@ export function useProjectList() {
const { data, status, refetch } = useQuery({
queryKey: PROJECT_LIST,
queryFn: getProjects,
placeholderData: placeholderProjectList,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt: number) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
@@ -13,7 +13,7 @@ export default function useRundown() {
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
queryKey: RUNDOWN,
queryFn: fetchNormalisedRundown,
placeholderData: cachedRundownPlaceholder,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchInterval,
@@ -10,7 +10,7 @@ export default function useSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: APP_SETTINGS,
queryFn: getSettings,
placeholderData: ontimePlaceholderSettings,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
@@ -8,7 +8,7 @@ export default function useUrlPresets() {
const { data, status, isError, refetch } = useQuery({
queryKey: URL_PRESETS,
queryFn: getUrlPresets,
placeholderData: [],
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
@@ -9,12 +9,12 @@ export default function useViewSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: VIEW_SETTINGS,
queryFn: getView,
placeholderData: viewsSettingsPlaceholder,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
});
return { data, status, isError, refetch, isFetching };
return { data: data ?? viewsSettingsPlaceholder, status, isError, refetch, isFetching };
}
@@ -0,0 +1,11 @@
import { useEffect } from 'react';
/**
* Sets tab title
* @param title
*/
export function useWindowTitle(title: string) {
useEffect(() => {
document.title = `ontime - ${title}`;
}, []);
}
@@ -3,7 +3,6 @@ import { create } from 'zustand';
import { booleanFromLocalStorage } from '../utils/localStorage';
type EditorSettings = {
showQuickEntry: boolean;
linkPrevious: boolean;
defaultPublic: boolean;
defaultDuration: string;
@@ -12,14 +11,12 @@ type EditorSettings = {
type EditorSettingsStore = {
eventSettings: EditorSettings;
setLocalEventSettings: (newState: EditorSettings) => void;
setShowQuickEntry: (showQuickEntry: boolean) => void;
setLinkPrevious: (linkPrevious: boolean) => void;
setDefaultPublic: (defaultPublic: boolean) => void;
setDefaultDuration: (defaultDuration: string) => void;
};
enum EditorSettingsKeys {
ShowQuickEntry = 'ontime-show-quick-entry',
LinkPrevious = 'ontime-link-previous',
DefaultPublic = 'ontime-default-public',
DefaultDuration = 'ontime-default-duration',
@@ -27,7 +24,6 @@ enum EditorSettingsKeys {
export const useEditorSettings = create<EditorSettingsStore>((set) => ({
eventSettings: {
showQuickEntry: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, false),
linkPrevious: booleanFromLocalStorage(EditorSettingsKeys.LinkPrevious, true),
defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.DefaultPublic, true),
defaultDuration: localStorage.getItem(EditorSettingsKeys.DefaultDuration) ?? '00:10:00',
@@ -35,18 +31,11 @@ export const useEditorSettings = create<EditorSettingsStore>((set) => ({
setLocalEventSettings: (value) =>
set(() => {
localStorage.setItem(EditorSettingsKeys.ShowQuickEntry, String(value.showQuickEntry));
localStorage.setItem(EditorSettingsKeys.LinkPrevious, String(value.linkPrevious));
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(value.defaultPublic));
return { eventSettings: value };
}),
setShowQuickEntry: (showQuickEntry) =>
set((state) => {
localStorage.setItem(EditorSettingsKeys.ShowQuickEntry, String(showQuickEntry));
return { eventSettings: { ...state.eventSettings, showQuickEntry } };
}),
setLinkPrevious: (linkPrevious) =>
set((state) => {
localStorage.setItem(EditorSettingsKeys.LinkPrevious, String(linkPrevious));
@@ -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>
@@ -13,7 +13,7 @@ export default function GeneralPanel({ location }: PanelBaseProps) {
return (
<>
<Panel.Header>Settings</Panel.Header>
<Panel.Header>App Settings</Panel.Header>
<div ref={manageRef}>
<GeneralPanelForm />
</div>
@@ -7,7 +7,6 @@ import * as Panel from '../PanelUtils';
export default function EditorSettingsForm() {
const eventSettings = useEditorSettings((state) => state.eventSettings);
const setShowQuickEntry = useEditorSettings((state) => state.setShowQuickEntry);
const setLinkPrevious = useEditorSettings((state) => state.setLinkPrevious);
const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic);
const setDefaultDuration = useEditorSettings((state) => state.setDefaultDuration);
@@ -19,53 +18,82 @@ export default function EditorSettingsForm() {
<Panel.Card>
<Panel.SubHeader>Editor settings</Panel.SubHeader>
<Panel.Divider />
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Show quick entry'
description='Whether the quick entry buttons show under selected event'
/>
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.showQuickEntry}
onChange={(event) => setShowQuickEntry(event.target.checked)}
/>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Link previous'
description='New events start time will be linked to the previous event'
/>
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.linkPrevious}
onChange={(event) => setLinkPrevious(event.target.checked)}
/>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Default duration'
description='When creating a new event, what is the default duration'
/>
<TimeInput<'defaultDuration'>
name='defaultDuration'
submitHandler={(_field, value) => setDefaultDuration(value)}
time={durationInMs}
placeholder='00:10:00'
/>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Default public' description='New events will be public' />
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.defaultPublic}
onChange={(event) => setDefaultPublic(event.target.checked)}
/>
</Panel.ListItem>
</Panel.ListGroup>
<Panel.Section>
<Panel.Title>Rundown options</Panel.Title>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Default duration'
description='When creating a new event, what is the default duration'
/>
<TimeInput<'defaultDuration'>
name='defaultDuration'
submitHandler={(_field, value) => setDefaultDuration(value)}
time={durationInMs}
placeholder='00:10:00'
/>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Link previous'
description='New events start time will be linked to the previous event'
/>
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.linkPrevious}
onChange={(event) => setLinkPrevious(event.target.checked)}
/>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Default public' description='New events will be public' />
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.defaultPublic}
onChange={(event) => setDefaultPublic(event.target.checked)}
/>
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
<Panel.Section>
<Panel.Title>Play mode</Panel.Title>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Show quick entry'
description='Whether the quick entry buttons show above / under selected event'
/>
<Switch variant='ontime' size='lg' defaultChecked={false} isDisabled />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Follow playback'
description='Whether view automatically follows the event being played'
/>
<Switch variant='ontime' size='lg' defaultChecked isDisabled />
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
<Panel.Section>
<Panel.Title>Edit mode</Panel.Title>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Show quick entry'
description='Whether the quick entry buttons show above / under selected event'
/>
<Switch variant='ontime' size='lg' defaultChecked isDisabled />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Follow playback'
description='Whether view automatically follows the event being played'
/>
<Switch variant='ontime' size='lg' defaultChecked={false} isDisabled />
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
</Panel.Card>
</Panel.Section>
);
@@ -43,8 +43,7 @@ export default function ProjectList() {
<thead>
<tr>
<th className={style.containCell}>Project Name</th>
<th>Date Created</th>
<th>Date Modified</th>
<th>Last Used</th>
<th />
</tr>
</thead>
@@ -53,7 +52,6 @@ export default function ProjectList() {
<ProjectListItem
key={project.filename}
filename={project.filename}
createdAt={project.createdAt}
updatedAt={project.updatedAt}
onToggleEditMode={handleToggleEditMode}
onSubmit={handleClear}
@@ -21,7 +21,6 @@ export type EditMode = 'rename' | 'duplicate' | null;
interface ProjectListItemProps {
current?: boolean;
filename: string;
createdAt: string;
updatedAt: string;
onToggleEditMode: (editMode: EditMode, filename: string | null) => void;
onSubmit: () => void;
@@ -32,14 +31,13 @@ interface ProjectListItemProps {
export default function ProjectListItem({
current,
createdAt,
updatedAt,
editingFilename,
editingMode,
filename,
onRefetch,
onSubmit,
onToggleEditMode,
updatedAt,
}: ProjectListItemProps) {
const [submitError, setSubmitError] = useState<string | null>(null);
@@ -102,7 +100,6 @@ export default function ProjectListItem({
) : (
<>
<td className={style.containCell}>{filename}</td>
<td>{new Date(createdAt).toLocaleString()}</td>
<td>{new Date(updatedAt).toLocaleString()}</td>
<td className={style.actionButton}>
<ActionMenu
@@ -11,6 +11,7 @@ import CustomFieldForm from './CustomFieldForm';
import style from './ProjectSettingsPanel.module.scss';
interface CustomFieldEntryProps {
field: string;
colour: string;
label: string;
onEdit: (label: CustomFieldLabel, patch: CustomField) => Promise<void>;
@@ -18,13 +19,11 @@ interface CustomFieldEntryProps {
}
export default function CustomFieldEntry(props: CustomFieldEntryProps) {
const { colour, label, onEdit, onDelete } = props;
const { colour, label, onEdit, onDelete, field } = props;
const [isEditing, setIsEditing] = useState(false);
const handleEdit = async (patch: CustomField) => {
const oldLabel = label;
await onEdit(oldLabel, patch);
await onEdit(field, patch);
setIsEditing(false);
};
@@ -64,7 +63,7 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
color='#FA5656' // $red-500
icon={<IoTrash />}
aria-label='Delete entry'
onClick={() => onDelete(label)}
onClick={() => onDelete(field)}
/>
</td>
</tr>
@@ -84,6 +84,7 @@ export default function ProjectSettingsPanel() {
return (
<CustomFieldEntry
key={key}
field={key}
colour={colour}
label={label}
onEdit={handleEditField}
@@ -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>
</>
);
}
@@ -1,9 +1,10 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { Button, Input } from '@chakra-ui/react';
import { Button, Input, Spinner } from '@chakra-ui/react';
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
import { IoShieldCheckmarkOutline } from '@react-icons/all-files/io5/IoShieldCheckmarkOutline';
import { getWorksheetNames } from '../../../../common/api/sheets';
import { maybeAxiosError } from '../../../../common/api/utils';
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
import { openLink } from '../../../../common/utils/linkUtils';
import * as Panel from '../PanelUtils';
@@ -29,7 +30,7 @@ export default function GSheetSetup(props: GSheetSetupProps) {
const sheetId = useSheetStore((state) => state.sheetId);
const setSheetId = useSheetStore((state) => state.setSheetId);
const setWorksheets = useSheetStore((state) => state.setWorksheets);
const patchStepData = useSheetStore((state) => state.patchStepData);
const authenticationStatus = useSheetStore((state) => state.authenticationStatus);
const setAuthenticationStatus = useSheetStore((state) => state.setAuthenticationStatus);
@@ -91,8 +92,13 @@ export default function GSheetSetup(props: GSheetSetupProps) {
setAuthenticationStatus(result.authenticated);
if (result.authenticated !== 'pending') {
if (result.authenticated == 'authenticated') {
const names = await getWorksheetNames(result.sheetId);
setWorksheets(names);
try {
const names = await getWorksheetNames(result.sheetId);
setWorksheets(names);
} catch (error) {
const message = maybeAxiosError(error);
patchStepData({ worksheet: { available: false, error: message } });
}
}
setLoading('');
return;
@@ -184,6 +190,7 @@ export default function GSheetSetup(props: GSheetSetupProps) {
) : (
<Panel.ListGroup>
<div className={style.buttonRow}>
{isAuthenticating && <Spinner />}
<CopyTag label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
{authKey ? authKey : 'Upload files to generate Auth Key'}
</CopyTag>
@@ -192,8 +199,7 @@ export default function GSheetSetup(props: GSheetSetupProps) {
size='sm'
leftIcon={<IoShieldCheckmarkOutline />}
onClick={handleAuthenticate}
isDisabled={!canAuthenticate || isLoading}
isLoading={loading === 'authenticate' || isAuthenticating}
isDisabled={!canAuthenticate}
>
Authenticate
</Button>
@@ -26,6 +26,7 @@
.buttonRow {
display: flex;
gap: 1rem;
align-items: center;
justify-content: end;
}
@@ -2,7 +2,7 @@ import { ChangeEvent, useRef, useState } from 'react';
import { Button, Input } from '@chakra-ui/react';
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
import { ImportMap, unpackError } from 'ontime-utils';
import { getErrorMessage, ImportMap } from 'ontime-utils';
import {
getWorksheetNames as getWorksheetNamesExcel,
@@ -59,7 +59,7 @@ export default function SourcesPanel() {
setImportFlow('excel');
setHasFile('done');
} catch (error) {
const errorMessage = unpackError(error);
const errorMessage = getErrorMessage(error);
setError(`Error uploading file: ${errorMessage}`);
setWorksheets(null);
setHasFile('none');
@@ -141,14 +141,13 @@ export default function ImportMapForm(props: ImportMapFormProps) {
size='sm'
{...register(label as keyof NamedImportMap)}
>
{worksheetNames &&
worksheetNames.map((name) => {
return (
<option key={name} value={name}>
{name}
</option>
);
})}
{worksheetNames?.map((name) => {
return (
<option key={name} value={name}>
{name}
</option>
);
})}
</Select>
</td>
<td className={style.singleActionCell} />
@@ -15,7 +15,7 @@ export const settingPanels: Readonly<SettingsOption[]> = [
},
{
id: 'general',
label: 'General',
label: 'App Settings',
secondary: [
{ id: 'general__manage', label: 'Manage Ontime settings' },
{ id: 'general__view', label: 'View settings' },
@@ -27,7 +27,7 @@ export const settingPanels: Readonly<SettingsOption[]> = [
label: 'Project Settings',
secondary: [{ id: 'project_settings__custom', label: 'Custom fields' }],
},
{ id: 'interface', label: 'Interface', secondary: [{ id: 'general__editor', label: 'Editor settings' }] },
{ id: 'interface', label: 'Interface', secondary: [{ id: 'interface__editor', label: 'Editor settings' }] },
{
id: 'sources',
label: 'Data Sources',
@@ -51,9 +51,15 @@ export const settingPanels: Readonly<SettingsOption[]> = [
label: 'About',
split: true,
},
{
id: 'shutdown',
label: 'Shutdown',
split: true,
},
] as const;
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
export interface PanelBaseProps {
location?: string;
}
@@ -52,7 +52,7 @@ export default function MessageControl() {
onClick={() => setMessage.timerBlink(!blink)}
data-testid='toggle timer blink'
>
Blink message
Blink
</Button>
<Button
size='sm'
@@ -66,7 +66,7 @@ export default function MessageControl() {
</Button>
</div>
<InputRow
label='External Message (readonly)'
label='External Message (read only)'
placeholder={enDash}
readonly
text={message.external.text || ''}
@@ -1,21 +1,14 @@
.tableWrapper {
width: 100%;
height: 100vh;
padding: 1rem;
padding: 1rem 0.5rem;
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,22 @@
import { useCallback, useEffect, useMemo } from 'react';
import { CustomFieldLabel, isOntimeEvent, ProjectData } from 'ontime-types';
import { useCallback, useMemo } from 'react';
import { IconButton, useDisclosure } from '@chakra-ui/react';
import { IoApps } from '@react-icons/all-files/io5/IoApps';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
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 { useWindowTitle } from '../../common/hooks/useWindowTitle';
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';
@@ -19,15 +24,14 @@ export default function CuesheetWrapper() {
// TODO: can we use the normalised rundown for the table?
const { data: flatRundown, status: rundownStatus } = useFlatRundown();
const { data: customFields } = useCustomFields();
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
const { updateCustomField } = useEventAction();
const featureData = useCuesheet();
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
// Set window title
useEffect(() => {
document.title = 'ontime - Cuesheet';
}, []);
useWindowTitle('Cuesheet');
/**
* Handles updating a field
@@ -74,38 +78,29 @@ 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} />
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
<Overview>
<IconButton
aria-label='Toggle settings'
variant='ontime-subtle-white'
size='lg'
icon={<IoApps />}
onClick={onOpen}
/>
<IconButton
aria-label='Toggle navigation'
variant='ontime-subtle-white'
size='lg'
icon={<IoSettingsOutline />}
onClick={() => toggleSettings()}
/>
</Overview>
<CuesheetProgress />
<Cuesheet
data={flatRundown}
@@ -1,4 +1,4 @@
import { memo } from 'react';
import { memo, useEffect } from 'react';
import { Tooltip } from '@chakra-ui/react';
import {
closestCenter,
@@ -31,6 +31,12 @@ function CuesheetHeader(props: CuesheetHeaderProps) {
const { headerGroups } = props;
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>('table-order', initialColumnOrder);
useEffect(() => {
if (!localStorage.getItem('table-order')) {
saveColumnOrder(initialColumnOrder);
}
}, [saveColumnOrder]);
const handleOnDragEnd = (event: DragEndEvent) => {
const { delta, active, over } = event;
@@ -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;
}
+63 -17
View File
@@ -1,10 +1,13 @@
import { lazy, useEffect } from 'react';
import { lazy, useCallback, useEffect } from 'react';
import { IconButton, useDisclosure } from '@chakra-ui/react';
import { IoApps } from '@react-icons/all-files/io5/IoApps';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
import useElectronEvent from '../../common/hooks/useElectronEvent';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
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';
@@ -14,27 +17,71 @@ const TimerControl = lazy(() => import('../control/playback/TimerControlExport')
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
export default function Editor() {
const { isOpen, setLocation, close } = useAppSettingsNavigation();
const { isOpen: isSettingsOpen, setLocation, close } = useAppSettingsNavigation();
const { isElectron } = useElectronEvent();
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
const handleSettings = (newTab?: SettingsOptionId) => {
if (isOpen) {
const toggleSettings = useCallback(() => {
if (isSettingsOpen) {
close();
} else {
setLocation(newTab ?? 'project');
setLocation('project');
}
};
}, [close, isSettingsOpen, setLocation]);
// Set window title
// 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 === ',') {
toggleSettings();
event.preventDefault();
event.stopPropagation();
}
}
},
[toggleSettings],
);
// register ctrl + , to open settings
useEffect(() => {
document.title = 'ontime - Editor';
}, []);
if (isElectron) {
document.addEventListener('keydown', handleKeyPress);
}
return () => {
if (isElectron) {
document.removeEventListener('keydown', handleKeyPress);
}
};
}, [handleKeyPress, isElectron]);
useWindowTitle('Editor');
return (
<div className={styles.mainContainer} data-testid='event-editor'>
<ErrorBoundary>
<MenuBar openSettings={handleSettings} isSettingsOpen={isOpen} />
</ErrorBoundary>
{isOpen ? (
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
<Overview>
<IconButton
aria-label='Toggle navigation'
variant='ontime-subtle-white'
size='lg'
icon={<IoApps />}
onClick={onOpen}
/>
<IconButton
aria-label='Toggle settings'
variant='ontime-subtle-white'
size='lg'
icon={<IoSettingsOutline />}
onClick={toggleSettings}
/>
</Overview>
{isSettingsOpen ? (
<AppSettings />
) : (
<div id='panels' className={styles.panelContainer}>
@@ -45,7 +92,6 @@ export default function Editor() {
<Rundown />
</div>
)}
<Overview />
</div>
);
}
@@ -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,12 +3,12 @@ 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 Empty from '../../common/components/state/Empty';
import { getOperatorOptions } from '../../common/components/view-params-editor/constants';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import useFollowComponent from '../../common/hooks/useFollowComponent';
import { useOperator } from '../../common/hooks/useSocket';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import useCustomFields from '../../common/hooks-query/useCustomFields';
import useProjectData from '../../common/hooks-query/useProjectData';
import useRundown from '../../common/hooks-query/useRundown';
@@ -57,10 +57,7 @@ export default function Operator() {
topOffset: selectedOffset,
});
// Set window title
useEffect(() => {
document.title = 'ontime - Operator';
}, []);
useWindowTitle('Operator');
// reset scroll if nothing is selected
useEffect(() => {
@@ -141,7 +138,6 @@ export default function Operator() {
return (
<div className={style.operatorContainer}>
<NavigationMenu />
<ViewParamsEditor paramFields={operatorOptions} />
{editEvent && <EditModal event={editEvent} onClose={() => setEditEvent(null)} />}
@@ -0,0 +1,29 @@
import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useDisclosure } from '@chakra-ui/react';
import FloatingNavigation from '../../common/components/navigation-menu/FloatingNavigation';
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
import Operator from './Operator';
export default function OperatorExport() {
const [searchParams, setSearchParams] = useSearchParams();
const { isOpen, onOpen, onClose } = useDisclosure();
const showEditFormDrawer = useCallback(() => {
searchParams.set('edit', 'true');
setSearchParams(searchParams);
}, [searchParams, setSearchParams]);
const toggleMenu = isOpen ? onClose : onOpen;
return (
<ProtectRoute permission='operator'>
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
<ProductionNavigationMenu isMenuOpen={isOpen} onMenuClose={onClose} />
<Operator />
</ProtectRoute>
);
}
@@ -1,10 +1,20 @@
.overview {
grid-area: overview;
font-size: $inner-section-text-size;
display: flex;
}
.nav {
display: flex;
gap: 0.5rem;
}
.info {
flex: 1;
padding-inline: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
font-size: $inner-section-text-size;
padding: 0 1rem;
}
.title {
+23 -47
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({ children }: { children: React.ReactNode }) {
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
@@ -44,15 +24,23 @@ export default function Overview() {
return (
<div className={style.overview}>
<ErrorBoundary>
<TitlesOverview />
<div className={style.column}>
<TimeRow label='Planned start' value={formatedTime(plannedStart)} className={style.start} />
<TimeRow label='Actual start' value={formatedTime(actualStart)} className={style.start} />
</div>
<RuntimeOverview />
<div className={style.column}>
<TimeRow label='Planned end' value={plannedEndText} className={style.end} daySpan={maybePlannedDaySpan} />
<TimeRow label='Expected end' value={expectedEndText} className={style.end} daySpan={maybeExpectedDaySpan} />
<div className={style.nav}>{children}</div>
<div className={style.info}>
<TitlesOverview />
<div>
<TimeRow label='Planned start' value={formatedTime(plannedStart)} className={style.start} />
<TimeRow label='Actual start' value={formatedTime(actualStart)} className={style.start} />
</div>
<RuntimeOverview />
<div>
<TimeRow label='Planned end' value={plannedEndText} className={style.end} daySpan={maybePlannedDaySpan} />
<TimeRow
label='Expected end'
value={expectedEndText}
className={style.end}
daySpan={maybeExpectedDaySpan}
/>
</div>
</div>
</ErrorBoundary>
</div>
@@ -63,25 +51,13 @@ function TitlesOverview() {
const { data } = useProjectData();
return (
<div className={style.titles}>
<div>
<div className={style.title}>{data.title}</div>
<div className={style.description}>{data.description}</div>
</div>
);
}
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;
}
+13 -3
View File
@@ -39,7 +39,6 @@ export default function Rundown({ data }: RundownProps) {
const eventSettings = useEditorSettings((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic;
const linkPrevious = eventSettings.linkPrevious;
const showQuickEntry = eventSettings.showQuickEntry;
// cursor
const { cursor, mode: appMode, setCursor } = useAppMode();
@@ -219,6 +218,8 @@ export default function Rundown({ data }: RundownProps) {
// all events before the current selected are in the past
let isPast = Boolean(featureData?.selectedEventId);
const isEditMode = appMode === AppMode.Edit;
return (
<div className={style.eventContainer} ref={scrollRef} data-testid='rundown'>
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
@@ -248,6 +249,7 @@ export default function Rundown({ data }: RundownProps) {
thisId = eventId;
}
}
const isFirst = index === 0;
const isLast = index === order.length - 1;
const isLoaded = featureData?.selectedEventId === event.id;
const isNext = featureData?.nextEventId === event.id;
@@ -258,6 +260,14 @@ export default function Rundown({ data }: RundownProps) {
return (
<Fragment key={event.id}>
{isEditMode && (hasCursor || isFirst) && (
<QuickAddBlock
showKbd={hasCursor ? 'above' : 'none'}
previousEventId={previousEventId}
disableAddDelay={isOntimeDelay(event)}
disableAddBlock={isOntimeBlock(event)}
/>
)}
<div className={style.entryWrapper} data-testid={`entry-${eventIndex}`}>
{isOntimeEvent(event) && <div className={style.entryIndex}>{eventIndex}</div>}
<div className={style.entry} key={event.id} ref={hasCursor ? cursorRef : undefined}>
@@ -277,9 +287,9 @@ export default function Rundown({ data }: RundownProps) {
/>
</div>
</div>
{((showQuickEntry && hasCursor) || isLast) && (
{isEditMode && (hasCursor || isLast) && (
<QuickAddBlock
showKbd={hasCursor}
showKbd={hasCursor ? 'below' : 'none'}
previousEventId={event.id}
disableAddDelay={isOntimeDelay(event)}
disableAddBlock={isOntimeBlock(event)}
@@ -13,7 +13,18 @@ import DelayBlock from './delay-block/DelayBlock';
import EventBlock from './event-block/EventBlock';
import { useEventSelection } from './useEventSelection';
export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'delete' | 'clone' | 'update' | 'swap';
export type EventItemActions =
| 'set-cursor'
| 'event'
| 'event-before'
| 'delay'
| 'delay-before'
| 'block'
| 'block-before'
| 'delete'
| 'clone'
| 'update'
| 'swap';
interface RundownEntryProps {
type: SupportedEvent;
@@ -83,12 +94,27 @@ export default function RundownEntry(props: RundownEntryProps) {
};
return addEvent(newEvent, options);
}
case 'event-before': {
const newEvent = { type: SupportedEvent.Event };
const options = {
after: previousEventId,
defaultPublic,
linkPrevious,
};
return addEvent(newEvent, options);
}
case 'delay': {
return addEvent({ type: SupportedEvent.Delay }, { after: data.id });
}
case 'delay-before': {
return addEvent({ type: SupportedEvent.Delay }, { after: previousEventId });
}
case 'block': {
return addEvent({ type: SupportedEvent.Block }, { after: data.id });
}
case 'block-before': {
return addEvent({ type: SupportedEvent.Block }, { after: previousEventId });
}
case 'swap': {
const { value } = payload as FieldValue;
return swapEvents({ from: value as string, to: data.id });
@@ -163,9 +189,9 @@ export default function RundownEntry(props: RundownEntryProps) {
/>
);
} else if (data.type === SupportedEvent.Block) {
return <BlockBlock data={data} hasCursor={hasCursor} actionHandler={actionHandler} />;
return <BlockBlock data={data} hasCursor={hasCursor} onDelete={() => actionHandler('delete')} />;
} else if (data.type === SupportedEvent.Delay) {
return <DelayBlock data={data} hasCursor={hasCursor} actionHandler={actionHandler} />;
return <DelayBlock data={data} hasCursor={hasCursor} />;
}
return null;
}
@@ -4,11 +4,10 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import { handleLinks } from '../../common/utils/linkUtils';
import { cx } from '../../common/utils/styleUtils';
import EventEditor from './event-editor/EventEditor';
import RundownWrapper from './RundownWrapper';
import style from './RundownExport.module.scss';
import EventEditorWrapper from './event-editor/EventEditorWrapper';
const RundownExport = () => {
const isExtracted = window.location.pathname.includes('/rundown');
@@ -26,7 +25,7 @@ const RundownExport = () => {
</div>
<div className={style.side}>
<ErrorBoundary>
<EventEditor />
<EventEditorWrapper />
</ErrorBoundary>
</div>
</div>
@@ -1,32 +1,24 @@
import { useEffect, useRef } from 'react';
import { useRef } from 'react';
import { IconButton } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { OntimeBlock, OntimeEvent } from 'ontime-types';
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
import { OntimeBlock } from 'ontime-types';
import { cx } from '../../../common/utils/styleUtils';
import EditableBlockTitle from '../common/EditableBlockTitle';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import type { EventItemActions } from '../RundownEntry';
import style from './BlockBlock.module.scss';
interface BlockBlockProps {
data: OntimeBlock;
hasCursor: boolean;
actionHandler: (
action: EventItemActions,
payload?:
| number
| {
field: keyof Omit<OntimeEvent, 'duration'> | 'durationOverride';
value: unknown;
},
) => void;
onDelete: () => void;
}
export default function BlockBlock(props: BlockBlockProps) {
const { data, hasCursor, actionHandler } = props;
const { data, hasCursor, onDelete } = props;
const handleRef = useRef<null | HTMLSpanElement>(null);
const {
@@ -45,12 +37,6 @@ export default function BlockBlock(props: BlockBlockProps) {
transition,
};
useEffect(() => {
if (hasCursor) {
handleRef?.current?.focus();
}
}, [hasCursor]);
const blockClasses = cx([style.block, hasCursor ? style.hasCursor : null]);
return (
@@ -59,7 +45,14 @@ export default function BlockBlock(props: BlockBlockProps) {
<IoReorderTwo />
</span>
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
<BlockActionMenu className={style.actionMenu} enableDelete actionHandler={actionHandler} />
<IconButton
aria-label='Delete'
size='sm'
icon={<IoTrash />}
variant='ontime-subtle'
color='#FA5656'
onClick={onDelete}
/>
</div>
);
}
@@ -5,32 +5,21 @@ import { CSS } from '@dnd-kit/utilities';
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { OntimeDelay, OntimeEvent } from 'ontime-types';
import { OntimeDelay } from 'ontime-types';
import DelayInput from '../../../common/components/input/delay-input/DelayInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { cx } from '../../../common/utils/styleUtils';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import type { EventItemActions } from '../RundownEntry';
import style from './DelayBlock.module.scss';
interface DelayBlockProps {
data: OntimeDelay;
hasCursor: boolean;
actionHandler: (
action: EventItemActions,
payload?:
| number
| {
field: keyof Omit<OntimeEvent, 'duration'> | 'durationOverride';
value: unknown;
},
) => void;
}
export default function DelayBlock(props: DelayBlockProps) {
const { data, hasCursor, actionHandler } = props;
const { data, hasCursor } = props;
const { applyDelay, deleteEvent } = useEventAction();
const handleRef = useRef<null | HTMLSpanElement>(null);
@@ -79,7 +68,6 @@ export default function DelayBlock(props: DelayBlockProps) {
<Button onClick={cancelDelayHandler} size='sm' leftIcon={<IoClose />} variant='ontime-subtle-white'>
Cancel
</Button>
<BlockActionMenu enableDelete actionHandler={actionHandler} />
</div>
</div>
);
@@ -9,7 +9,7 @@ $skip-opacity: 0.2;
display: grid;
grid-template-areas:
'binder ... ... ...'
'binder pb-actions times actions'
'binder pb-actions times ...'
'binder pb-actions title title'
'binder pb-actions estatus estatus'
'binder ... ... ...';
@@ -55,7 +55,6 @@ $skip-opacity: 0.2;
outline: 1px solid $block-cursor-color;
}
/* we stop the eventActions from having opacity to fix issue with dropdown drawing order */
&.past:not(.skip) {
.timerNote,
.statusElements,
@@ -153,12 +152,6 @@ $skip-opacity: 0.2;
}
}
.eventActions {
grid-area: actions;
height: 100%;
text-align: end;
}
.progressBg {
grid-area: progb;
border-radius: 1px;
@@ -2,16 +2,18 @@ import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoCopyOutline } from '@react-icons/all-files/io5/IoCopyOutline';
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
import { EndAction, MaybeNumber, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
import { useContextMenu } from '../../../common/hooks/useContextMenu';
import { useAppMode } from '../../../common/stores/appModeStore';
import copyToClipboard from '../../../common/utils/copyToClipboard';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import type { EventItemActions } from '../RundownEntry';
import { useEventIdSwapping } from '../useEventIdSwapping';
@@ -96,31 +98,25 @@ export default function EventBlock(props: EventBlockProps) {
selectedEvents.size > 1
? [
{
label: 'Visiblity',
group: [
{
label: 'Make public',
icon: IoPeople,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: true,
}),
},
{
label: 'Make private',
icon: IoPeopleOutline,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: false,
}),
},
],
label: 'Make public',
icon: IoPeople,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: true,
}),
},
{
label: 'Make private',
icon: IoPeopleOutline,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: false,
}),
},
]
: [
{ label: `Copy ID: ${eventId}`, icon: IoCopyOutline, onClick: () => copyToClipboard(eventId) },
{
label: 'Toggle public',
icon: IoPeopleOutline,
@@ -145,6 +141,14 @@ export default function EventBlock(props: EventBlockProps) {
},
isDisabled: selectedEventId == null || selectedEventId === eventId,
},
{ withDivider: true, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') },
{ withDivider: true, label: 'Event before', icon: IoAdd, onClick: () => actionHandler('event-before') },
{ label: 'Event after', icon: IoAdd, onClick: () => actionHandler('event') },
{ label: 'Block before', icon: IoRemoveCircleOutline, onClick: () => actionHandler('block-before') },
{ label: 'Block after', icon: IoRemoveCircleOutline, onClick: () => actionHandler('block') },
{ label: 'Delay before', icon: IoTimerOutline, onClick: () => actionHandler('delay-before') },
{ label: 'Delay after', icon: IoTimerOutline, onClick: () => actionHandler('delay') },
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
],
);
@@ -281,7 +285,6 @@ export default function EventBlock(props: EventBlockProps) {
loaded={loaded}
playback={playback}
isRolling={isRolling}
actionHandler={actionHandler}
/>
)}
</div>
@@ -14,10 +14,8 @@ import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontim
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import EditableBlockTitle from '../common/EditableBlockTitle';
import { EventItemActions } from '../RundownEntry';
import TimeInputFlow from '../time-input-flow/TimeInputFlow';
import BlockActionMenu from './composite/BlockActionMenu';
import EventBlockPlayback from './composite/EventBlockPlayback';
import EventBlockProgressBar from './composite/EventBlockProgressBar';
@@ -46,7 +44,6 @@ interface EventBlockInnerProps {
loaded: boolean;
playback?: Playback;
isRolling: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void;
}
const EventBlockInner = (props: EventBlockInnerProps) => {
@@ -68,7 +65,6 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
loaded,
playback,
isRolling,
actionHandler,
} = props;
const [renderInner, setRenderInner] = useState(false);
@@ -139,9 +135,6 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
</Tooltip>
</div>
</div>
<div className={style.eventActions}>
<BlockActionMenu showClone enableDelete={!loaded} actionHandler={actionHandler} />
</div>
</>
);
};
@@ -1,64 +0,0 @@
import { useCallback } from 'react';
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Tooltip } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { IoTrashBinSharp } from '@react-icons/all-files/io5/IoTrashBinSharp';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import { EventItemActions } from '../../RundownEntry';
interface BlockActionMenuProps {
enableDelete?: boolean;
showClone?: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void;
className?: string;
}
export default function BlockActionMenu(props: BlockActionMenuProps) {
const { enableDelete, showClone, actionHandler, className } = props;
const handleAddEvent = useCallback(() => actionHandler('event'), [actionHandler]);
const handleAddDelay = useCallback(() => actionHandler('delay'), [actionHandler]);
const handleAddBlock = useCallback(() => actionHandler('block'), [actionHandler]);
const handleClone = useCallback(() => actionHandler('clone'), [actionHandler]);
const handleDelete = useCallback(() => actionHandler('delete'), [actionHandler]);
return (
<Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<Tooltip label='Add ...' openDelay={tooltipDelayMid}>
<MenuButton
as={IconButton}
aria-label='Event options'
icon={<IoEllipsisHorizontal />}
tabIndex={-1}
variant='ontime-ghosted-white'
size='sm'
className={className}
/>
</Tooltip>
<MenuList>
<MenuItem icon={<IoAdd />} onClick={handleAddEvent}>
Add Event after
</MenuItem>
<MenuItem icon={<IoTimerOutline />} onClick={handleAddDelay}>
Add Delay after
</MenuItem>
<MenuItem icon={<IoRemoveCircleOutline />} onClick={handleAddBlock}>
Add Block after
</MenuItem>
{showClone && (
<MenuItem icon={<IoDuplicateOutline />} onClick={handleClone}>
Clone event
</MenuItem>
)}
<MenuDivider />
<MenuItem icon={<IoTrashBinSharp />} onClick={handleDelete} isDisabled={!enableDelete} color='#D20300'>
Delete
</MenuItem>
</MenuList>
</Menu>
);
}
@@ -1,14 +1,11 @@
import { CSSProperties, useCallback, useEffect, useState } from 'react';
import { CSSProperties } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Button } from '@chakra-ui/react';
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
import { CustomFieldLabel, OntimeEvent } from 'ontime-types';
import CopyTag from '../../../common/components/copy-tag/CopyTag';
import { useEventAction } from '../../../common/hooks/useEventAction';
import useCustomFields from '../../../common/hooks-query/useCustomFields';
import useRundown from '../../../common/hooks-query/useRundown';
import { getAccessibleColour } from '../../../common/utils/styleUtils';
import { useEventSelection } from '../useEventSelection';
import EventEditorTimes from './composite/EventEditorTimes';
import EventEditorTitles from './composite/EventEditorTitles';
@@ -20,48 +17,16 @@ export type EventEditorSubmitActions = keyof OntimeEvent;
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
export default function EventEditor() {
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data } = useRundown();
interface EventEditorProps {
event: OntimeEvent | null;
handleSubmit: (field: EditorUpdateFields, value: string) => void;
isMultiple: boolean;
}
export default function EventEditor({ event, handleSubmit, isMultiple }: EventEditorProps) {
const { data: customFields } = useCustomFields();
const { order, rundown } = data;
const { updateEvent } = useEventAction();
const [_searchParams, setSearchParams] = useSearchParams();
const [event, setEvent] = useState<OntimeEvent | null>(null);
useEffect(() => {
if (order.length === 0) {
setEvent(null);
return;
}
const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
if (!selectedEventId) {
setEvent(null);
return;
}
const event = rundown[selectedEventId];
if (event && isOntimeEvent(event)) {
setEvent(event);
} else {
setEvent(null);
}
}, [order, rundown, selectedEvents]);
const handleSubmit = useCallback(
(field: EditorUpdateFields, value: string) => {
if (field.startsWith('custom-')) {
const fieldLabel = field.split('custom-')[1];
updateEvent({ id: event?.id, custom: { [fieldLabel]: { value } } });
} else {
updateEvent({ id: event?.id, [field]: value });
}
},
[event?.id, updateEvent],
);
const handleOpenCustomManager = () => {
setSearchParams({ settings: 'project_settings__custom' });
};
@@ -100,6 +65,7 @@ export default function EventEditor() {
note={event.note}
colour={event.colour}
handleSubmit={handleSubmit}
isMultiple={isMultiple}
/>
<div className={style.column}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
@@ -128,10 +94,12 @@ export default function EventEditor() {
})}
</div>
</div>
<div className={style.footer}>
<CopyTag label='OSC trigger by id'>{`/ontime/load/id "${event.id}"`}</CopyTag>
<CopyTag label='OSC trigger by cue'>{`/ontime/load/cue "${event.cue}"`}</CopyTag>
</div>
{!isMultiple ? (
<div className={style.footer}>
<CopyTag label='OSC trigger by id'>{`/ontime/load/id "${event.id}"`}</CopyTag>
<CopyTag label='OSC trigger by cue'>{`/ontime/load/cue "${event.cue}"`}</CopyTag>
</div>
) : null}
</div>
);
}
@@ -0,0 +1,140 @@
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
CustomFieldLabel,
EndAction,
isOntimeEvent,
OntimeEvent,
SupportedEvent,
TimerType,
TimeStrategy,
} from 'ontime-types';
import { useEventAction } from '../../../common/hooks/useEventAction';
import useRundown from '../../../common/hooks-query/useRundown';
import { useEventSelection } from '../useEventSelection';
import style from './EventEditor.module.scss';
import EventEditor from './EventEditor';
export type EventEditorSubmitActions = keyof OntimeEvent;
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
export default function EventEditorWrapper() {
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data } = useRundown();
const { order, rundown } = data;
const { updateEvent } = useEventAction();
const [_searchParams] = useSearchParams();
const [event, setEvent] = useState<OntimeEvent | null>(null);
useEffect(() => {
if (order.length === 0) {
setEvent(null);
return;
}
const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
if (!selectedEventId) {
setEvent(null);
return;
}
const event = rundown[selectedEventId];
if (event && isOntimeEvent(event)) {
setEvent(event);
} else {
setEvent(null);
}
}, [order, rundown, selectedEvents]);
const handleSingleSubmit = useCallback(
(field: EditorUpdateFields, value: string) => {
if (field.startsWith('custom-')) {
const fieldLabel = field.split('custom-')[1];
updateEvent({ id: event?.id, custom: { [fieldLabel]: { value } } });
} else {
updateEvent({ id: event?.id, [field]: value });
}
},
[event?.id, updateEvent],
);
const handleMultipleSubmits = useCallback(
(field: EditorUpdateFields, value: string) => {
if (field.startsWith('custom-')) {
// const fieldLabel = field.split('custom-')[1];
// updateEvent({ id: event?.id, custom: { [fieldLabel]: { value } } });
} else {
// updateEvent({ id: event?.id, [field]: value });
}
},
[event?.id, updateEvent],
);
if (!event) {
return (
<div className={style.eventEditor} data-testid='editor-container'>
Select an event to edit
</div>
);
}
const getMultipleEvent = (): OntimeEvent => {
const allHaveSameValue = (arr: any[], propertyName: string) => {
if (!arr || arr.length <= 0) return false;
return arr.every((obj) => JSON.stringify(obj[propertyName]) === JSON.stringify(arr[0][propertyName]));
};
const events: OntimeEvent[] = [];
let eventsIds: string = '';
for (const event of selectedEvents) {
const data = rundown[event];
if (data && isOntimeEvent(data)) {
events.push(data);
eventsIds += `${event} `;
}
}
const multipleEvents: Partial<OntimeEvent> = {
id: eventsIds.trim().split(' ').join(', '),
colour: allHaveSameValue(events, 'colour') ? events[0]['colour'] : 'unkown',
custom: allHaveSameValue(events, 'custom') ? events[0]['custom'] : undefined,
duration: allHaveSameValue(events, 'duration') ? events[0]['duration'] : undefined,
endAction: allHaveSameValue(events, 'endAction') ? events[0]['endAction'] : EndAction.Unkown,
delay: allHaveSameValue(events, 'delay') ? events[0]['delay'] : undefined,
isPublic: allHaveSameValue(events, 'isPublic') ? events[0]['isPublic'] : true, // ??
linkStart: allHaveSameValue(events, 'linkStart') ? events[0]['linkStart'] : null,
note: allHaveSameValue(events, 'note') ? events[0]['note'] : '', // ??
revision: allHaveSameValue(events, 'revision') ? events[0]['revision'] : undefined,
skip: allHaveSameValue(events, 'skip') ? events[0]['skip'] : undefined,
timeDanger: allHaveSameValue(events, 'timeDanger') ? events[0]['timeDanger'] : undefined,
timeEnd: allHaveSameValue(events, 'timeEnd') ? events[0]['timeEnd'] : undefined,
timeStart: allHaveSameValue(events, 'timeStart') ? events[0]['timeStart'] : undefined,
timeStrategy: allHaveSameValue(events, 'timeStrategy') ? events[0]['timeStrategy'] : TimeStrategy.Unkown,
timeWarning: allHaveSameValue(events, 'timeWarning') ? events[0]['timeWarning'] : undefined,
timerType: allHaveSameValue(events, 'timerType') ? events[0]['timerType'] : TimerType.Unkown,
title: allHaveSameValue(events, 'title') ? events[0]['title'] : '', // ??
type: allHaveSameValue(events, 'type') ? events[0]['type'] : SupportedEvent.Event,
cue: allHaveSameValue(events, 'cue') ? events[0]['cue'] : 'unkown',
};
return {
...event,
...multipleEvents,
};
};
return (
<div className={style.eventEditor} data-testid='editor-container'>
{selectedEvents.size <= 1 ? (
<EventEditor event={event} handleSubmit={handleSingleSubmit} isMultiple={false} />
) : (
<EventEditor event={getMultipleEvent()} handleSubmit={handleMultipleSubmits} isMultiple={true} />
)}
</div>
);
}
@@ -3,7 +3,7 @@ import { Input } from '@chakra-ui/react';
import { sanitiseCue } from 'ontime-utils';
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
import { type EditorUpdateFields } from '../EventEditor';
import { type EditorUpdateFields } from '../EventEditorWrapper';
import EventTextArea from './EventTextArea';
import EventTextInput from './EventTextInput';
@@ -17,10 +17,11 @@ interface EventEditorTitlesProps {
note: string;
colour: string;
handleSubmit: (field: EditorUpdateFields, value: string) => void;
isMultiple?: boolean;
}
const EventEditorTitles = (props: EventEditorTitlesProps) => {
const { eventId, cue, title, note, colour, handleSubmit } = props;
const { eventId, cue, title, note, colour, handleSubmit, isMultiple } = props;
const cueSubmitHandler = (_field: string, newValue: string) => {
handleSubmit('cue', sanitiseCue(newValue));
@@ -46,7 +47,7 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
</div>
<div>
<label className={style.inputLabel}>Colour</label>
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} isMultiple={isMultiple} />
</div>
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
@@ -3,7 +3,7 @@ import { CSSProperties, useCallback } from 'react';
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
import { cx } from '../../../../common/utils/styleUtils';
import { EditorUpdateFields } from '../EventEditor';
import { EditorUpdateFields } from '../EventEditorWrapper';
import style from '../EventEditor.module.scss';
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { Input, InputProps } from '@chakra-ui/react';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
import { EditorUpdateFields } from '../EventEditor';
import { EditorUpdateFields } from '../EventEditorWrapper';
import style from '../EventEditor.module.scss';
@@ -23,7 +23,7 @@
padding: 0 0.25rem;
color: $label-gray;
border-radius: 2px;
background-color: $black-10
background-color: $black-10;
}
.options {
@@ -12,14 +12,14 @@ import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './QuickAddBlock.module.scss';
interface QuickAddBlockProps {
showKbd: boolean;
previousEventId: string;
showKbd: 'above' | 'below' | 'none';
previousEventId?: string;
disableAddDelay?: boolean;
disableAddBlock: boolean;
}
const QuickAddBlock = (props: QuickAddBlockProps) => {
const { showKbd, previousEventId, disableAddDelay = true, disableAddBlock } = props;
const { showKbd = 'none', previousEventId, disableAddDelay = true, disableAddBlock } = props;
const { addEvent } = useEventAction();
const { emitError } = useEmitLog();
@@ -28,6 +28,8 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
const { defaultPublic, linkPrevious } = useEditorSettings((state) => state.eventSettings);
const shortcutBase = showKbd === 'none' ? '' : `${deviceAlt} ${showKbd === 'above' ? '⇧' : ''}`;
const handleCreateEvent = useCallback(
(eventType: SupportedEvent) => {
switch (eventType) {
@@ -70,6 +72,9 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
[previousEventId, addEvent, emitError],
);
const canLinkPrevious = Boolean(previousEventId);
const shouldLinkPrevious = Boolean(linkPrevious) && canLinkPrevious;
return (
<div className={style.quickAdd}>
<div className={style.btnRow}>
@@ -79,10 +84,9 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
size='xs'
variant='ontime-subtle-white'
className={style.quickBtn}
data-testid='quick-add-event'
leftIcon={<IoAdd />}
>
Event {showKbd && <span className={style.keyboard}>{`${deviceAlt} + E`}</span>}
Event {shortcutBase && <span className={style.keyboard}>{`${shortcutBase} E`}</span>}
</Button>
</Tooltip>
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
@@ -92,10 +96,9 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
variant='ontime-subtle-white'
disabled={disableAddDelay}
className={style.quickBtn}
data-testid='quick-add-delay'
leftIcon={<IoAdd />}
>
Delay {showKbd && <span className={style.keyboard}>{`${deviceAlt} + D`}</span>}
Delay {shortcutBase && <span className={style.keyboard}>{`${shortcutBase} D`}</span>}
</Button>
</Tooltip>
<Tooltip label='Add Block' openDelay={tooltipDelayMid}>
@@ -105,15 +108,20 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
variant='ontime-subtle-white'
disabled={disableAddBlock}
className={style.quickBtn}
data-testid='quick-add-block'
leftIcon={<IoAdd />}
>
Block {showKbd && <span className={style.keyboard}>{`${deviceAlt} + B`}</span>}
Block {shortcutBase && <span className={style.keyboard}>{`${shortcutBase} B`}</span>}
</Button>
</Tooltip>
</div>
<div className={style.options}>
<Checkbox ref={doLinkPrevious} size='sm' variant='ontime-ondark' defaultChecked={linkPrevious}>
<Checkbox
ref={doLinkPrevious}
size='sm'
variant='ontime-ondark'
isDisabled={!canLinkPrevious}
defaultChecked={shouldLinkPrevious}
>
Link to previous
</Checkbox>
<Checkbox ref={doPublic} size='sm' variant='ontime-ondark' defaultChecked={defaultPublic}>
@@ -1,8 +1,6 @@
import { Button, ButtonGroup, MenuButton } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { ButtonGroup } from '@chakra-ui/react';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoSnowOutline } from '@react-icons/all-files/io5/IoSnowOutline';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';
@@ -16,20 +14,10 @@ export default function RundownHeader() {
const setAppMode = useAppMode((state) => state.setMode);
const setRunMode = () => setAppMode(AppMode.Run);
const setEditMode = () => setAppMode(AppMode.Edit);
const setFreezeMode = () => setAppMode(AppMode.Freeze);
return (
<div className={style.header}>
<ButtonGroup isAttached>
<TooltipActionBtn
variant={appMode === AppMode.Freeze ? 'ontime-filled' : 'ontime-outlined'}
size='sm'
icon={<IoSnowOutline />}
clickHandler={setFreezeMode}
tooltip='Freeze rundown'
aria-label='Freeze rundown'
isDisabled
/>
<TooltipActionBtn
variant={appMode === AppMode.Run ? 'ontime-filled' : 'ontime-outlined'}
size='sm'
@@ -47,11 +35,7 @@ export default function RundownHeader() {
aria-label='Edit mode'
/>
</ButtonGroup>
<RundownMenu>
<MenuButton size='sm' as={Button} rightIcon={<IoAdd />} aria-label='Rundown menu' variant='ontime-outlined'>
Rundown
</MenuButton>
</RundownMenu>
<RundownMenu />
</div>
);
}
@@ -1,31 +1,16 @@
import { memo, ReactNode, useCallback } from 'react';
import { Menu, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { IoTrashOutline } from '@react-icons/all-files/io5/IoTrashOutline';
import { SupportedEvent } from 'ontime-types';
import { useCallback } from 'react';
import { Button } from '@chakra-ui/react';
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { useAppMode } from '../../../common/stores/appModeStore';
import { useEventSelection } from '../useEventSelection';
const RundownMenu = ({ children }: { children: ReactNode }) => {
export default function RundownMenu() {
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
const setCursor = useAppMode((state) => state.setCursor);
const { addEvent, deleteAllEvents } = useEventAction();
const newEvent = useCallback(() => {
addEvent({ type: SupportedEvent.Event });
}, [addEvent]);
const newBlock = useCallback(() => {
addEvent({ type: SupportedEvent.Block });
}, [addEvent]);
const newDelay = useCallback(() => {
addEvent({ type: SupportedEvent.Delay });
}, [addEvent]);
const appMode = useAppMode((state) => state.mode);
const { deleteAllEvents } = useEventAction();
const deleteAll = useCallback(() => {
deleteAllEvents();
@@ -34,25 +19,15 @@ const RundownMenu = ({ children }: { children: ReactNode }) => {
}, [clearSelectedEvents, deleteAllEvents, setCursor]);
return (
<Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark' placement='right-start'>
{children}
<MenuList>
<MenuItem icon={<IoAdd />} onClick={newEvent}>
Add event at start
</MenuItem>
<MenuItem icon={<IoTimerOutline />} onClick={newDelay}>
Add delay at start
</MenuItem>
<MenuItem icon={<IoRemoveCircleOutline />} onClick={newBlock}>
Add block at start
</MenuItem>
<MenuDivider />
<MenuItem icon={<IoTrashOutline />} onClick={deleteAll} color='#D20300'>
Delete all events
</MenuItem>
</MenuList>
</Menu>
<Button
size='sm'
variant='ontime-outlined'
leftIcon={<IoTrash />}
onClick={deleteAll}
color='#FA5656'
isDisabled={appMode === 'run'}
>
Clear rundown
</Button>
);
};
export default memo(RundownMenu);
}
@@ -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';
@@ -15,6 +14,7 @@ import TitleCard from '../../../common/components/title-card/TitleCard';
import { getBackstageOptions } from '../../../common/components/view-params-editor/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
@@ -58,10 +58,7 @@ export default function Backstage(props: BackstageProps) {
const [blinkClass, setBlinkClass] = useState(false);
const [searchParams] = useSearchParams();
// Set window title
useEffect(() => {
document.title = 'ontime - Backstage Screen';
}, []);
useWindowTitle('Backstage');
// blink on change
useEffect(() => {
@@ -102,7 +99,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}
@@ -1,12 +1,11 @@
import { useEffect } from 'react';
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';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { OverridableOptions } from '../../../common/models/View.types';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
@@ -26,9 +25,7 @@ export default function Clock(props: ClockProps) {
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams();
useEffect(() => {
document.title = 'ontime - Clock';
}, []);
useWindowTitle('Clock');
// defer rendering until we load stylesheets
if (!shouldRender) {
@@ -133,7 +130,6 @@ export default function Clock(props: ClockProps) {
}}
data-testid='clock-view'
>
<NavigationMenu />
<ViewParamsEditor paramFields={clockOptions} />
<SuperscriptTime
time={clock}
@@ -4,10 +4,10 @@ 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';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
@@ -38,9 +38,7 @@ export default function Countdown(props: CountdownProps) {
const [runningMessage, setRunningMessage] = useState<TimerMessage>(TimerMessage.unhandled);
const [delay, setDelay] = useState(0);
useEffect(() => {
document.title = 'ontime - Countdown';
}, []);
useWindowTitle('Countdown');
// eg. http://localhost:4001/countdown?eventId=ei0us
// Check for user options
@@ -110,7 +108,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,10 +3,10 @@ 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';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { getPropertyValue } from '../common/viewUtils';
import './LowerThird.scss';
@@ -142,10 +142,7 @@ export default function LowerThird(props: LowerProps) {
const [playState, setPlayState] = useState<'pre' | 'in' | 'out'>('pre');
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
// set window title
useEffect(() => {
document.title = 'ontime - Lower Third';
}, []);
useWindowTitle('Lower Third');
const trigger = useMemo(() => {
if (options.trigger === TriggerType.Event) {
@@ -191,7 +188,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}`}
@@ -1,13 +1,12 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
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';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { OverridableOptions } from '../../../common/models/View.types';
import { timerPlaceholder } from '../../../common/utils/styleUtils';
@@ -29,9 +28,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useEffect(() => {
document.title = 'ontime - Minimal Timer';
}, []);
useWindowTitle('Minimal Timer');
// defer rendering until we load stylesheets
if (!shouldRender) {
@@ -186,7 +183,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'}>
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import QRCode from 'react-qr-code';
import { useSearchParams } from 'react-router-dom';
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';
@@ -13,6 +11,7 @@ import TitleCard from '../../../common/components/title-card/TitleCard';
import { getPublicOptions } from '../../../common/components/view-params-editor/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider';
@@ -55,10 +54,7 @@ export default function Public(props: BackstageProps) {
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
// set window title
useEffect(() => {
document.title = 'ontime - Public Screen';
}, []);
useWindowTitle('Public Schedule');
// defer rendering until we load stylesheets
if (!shouldRender) {
@@ -78,7 +74,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}
@@ -1,15 +1,14 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import type { OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-types';
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';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
@@ -47,9 +46,7 @@ export default function StudioClock(props: StudioClockProps) {
const [searchParams] = useSearchParams();
useEffect(() => {
document.title = 'ontime - Studio Clock';
}, []);
useWindowTitle('Studio Clock');
let clock = formatTime(time.clock);
let hasAmPm = '';
@@ -78,7 +75,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>}
@@ -32,6 +32,8 @@
.blackout {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000;
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import {
@@ -15,11 +14,11 @@ 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';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { timerPlaceholder } from '../../../common/utils/styleUtils';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
@@ -64,9 +63,7 @@ export default function Timer(props: TimerProps) {
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useEffect(() => {
document.title = 'ontime - Timer';
}, []);
useWindowTitle('Timer');
// defer rendering until we load stylesheets
if (!shouldRender) {
@@ -162,7 +159,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
+13 -1
View File
@@ -3,12 +3,24 @@ export const ontimeCheckboxOnDark = {
border: '1px',
borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100
_disabled: {
color: 'white',
borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100
opacity: 0.6,
},
_checked: {
borderColor: '#3182ce', // $action-blue
backgroundColor: '#3182ce', //$action-blue
_disabled: {
color: 'white',
borderColor: '#3182ce', // $action-blue
backgroundColor: '#3182ce', //$action-blue
opacity: 0.6,
},
},
_focus: {
boxShadow: '0 0 0 1px #578AF4', // $blue-500
boxShadow: 'none',
},
},
label: {
+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
},
},
};
+15 -7
View File
@@ -1,19 +1,27 @@
export const ontimeMenuOnDark = {
list: {
fontSize: 'calc(1rem - 2px)',
borderRadius: '3px',
border: 'none',
bg: '#fff', // $gray-50
borderColor: 'rgba(255, 255, 255, 0.1)',
color: '#ececec', // $gray-1030
backgroundColor: '#202020', // $gray-1250
zIndex: 100,
},
item: {
letterSpacing: '0.15px',
color: '#101010', // $gray-1350
bg: '#fff', //
backgroundColor: 'transparent',
paddingBlock: '0.5rem',
_hover: {
backgroundColor: '#e2e2e2', // $gray-200
backgroundColor: 'rgba(0, 0, 0, 0.1)',
_disabled: {
backgroundColor: 'transparent',
},
},
_disabled: {
color: '#b1b1b1', // $gray-400
},
},
divider: {
borderColor: '#cfcfcf', // $gray-200
borderColor: 'rgba(255, 255, 255, 0.07)',
opacity: 1,
},
};
+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: {
+2 -1
View File
@@ -43,9 +43,10 @@
"nodemon": "^2.0.20",
"ontime-types": "workspace:*",
"prettier": "^3.0.3",
"server-timing": "^3.3.3",
"shx": "^0.3.4",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"typescript": "^5.4.3",
"vitest": "^1.2.2"
},
"scripts": {
+7 -9
View File
@@ -116,16 +116,14 @@ export class SocketServer implements IAdapter {
// Protocol specific stuff handled above
try {
const reply = dispatchFromAdapter(
type,
{
payload,
},
'ws',
);
const reply = dispatchFromAdapter(type, { payload }, 'ws');
if (reply) {
const { payload } = reply;
ws.send(type, payload);
ws.send(
JSON.stringify({
type: 'ontime-change',
payload: reply.payload,
}),
);
}
} catch (error) {
logger.error(LogOrigin.Rx, `WS IN: ${error}`);
@@ -1,7 +1,8 @@
import { CustomField, CustomFields } from 'ontime-types';
import { CustomField, CustomFields, ErrorResponse } from 'ontime-types';
import type { Request, Response } from 'express';
import { getErrorMessage } from 'ontime-utils';
import {
createCustomField,
editCustomField,
@@ -14,36 +15,37 @@ export async function getCustomFields(_req: Request, res: Response<CustomFields>
res.json(customFields);
}
// Expects { label: <label> type: 'string | ..' }
export async function postCustomField(req: Request, res: Response) {
export async function postCustomField(req: Request, res: Response<CustomFields | ErrorResponse>) {
try {
const newField = req.body as CustomField;
const allFields = await createCustomField(newField);
res.status(201).send(allFields);
} catch (error) {
res.status(400).send({ message: error.toString() });
const message = getErrorMessage(error);
res.status(400).send({ message });
}
}
// Expects { label: <oldLabel>, field: { label: <newLabel> type: 'string | ..' } }
export async function putCustomField(req: Request, res: Response) {
export async function putCustomField(req: Request, res: Response<CustomFields | ErrorResponse>) {
try {
const oldLabel = req.params.label;
const { colour, type, label } = req.body;
const newFields = await editCustomField(oldLabel, { label, colour, type });
res.status(200).send(newFields);
} catch (error) {
res.status(400).send({ message: error.toString() });
const message = getErrorMessage(error);
res.status(400).send({ message });
}
}
// Expects { label: <label> }
export async function deleteCustomField(req: Request, res: Response) {
export async function deleteCustomField(req: Request, res: Response<CustomFields | ErrorResponse>) {
try {
const fieldToDelete = req.params.label;
await removeCustomField(fieldToDelete);
res.sendStatus(204);
} catch (error) {
res.status(400).send({ message: error.toString() });
const message = getErrorMessage(error);
res.status(400).send({ message });
}
}

Some files were not shown because too many files have changed in this diff Show More