* chore: upgrade minor versions
* chore: upgrade cypress
* chore: upgrade fe dependencies
* update readme
* update osx images
* update readme
* upgrade dependencies
* upgrade test dependency
* feat: add option to input autofill
* feat: autofill to the left
* chore: update docs
* chore: cleanup ci
* version bump
* style: prevent zero height bar
* fix: prevent loosing menu
* ux: improve input, no spellcheck or autocomplete
* small ux improvements in cuesheets
* feat 111/increase maximum number of events
* fix: optimistic delete issue with filter
* refact: pincode workflow
* chore: add versioning to packages
This commit is contained in:
Carlos Valente
2022-05-19 10:48:56 +02:00
committed by GitHub
parent 45ac44b2e8
commit 6ac963684d
155 changed files with 3481 additions and 3465 deletions
@@ -1,11 +1,10 @@
import React from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import VisibleIconBtn from '../../../common/components/buttons/VisibleIconBtn';
import { Tooltip } from '@chakra-ui/tooltip';
import { IconButton } from '@chakra-ui/button';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import style from './MessageControl.module.scss';
const inputProps = {
size: 'sm',
};
import PropTypes from 'prop-types';
export default function InputRow(props) {
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
@@ -24,12 +23,26 @@ export default function InputRow(props) {
<EditablePreview className={style.padleft} />
<EditableInput className={style.padleft} />
</Editable>
<VisibleIconBtn
active={visible || undefined}
actionHandler={actionHandler}
{...inputProps}
/>
<Tooltip label={visible ? 'Make invisible' : 'Make visible'} openDelay={500}>
<IconButton
aria-label='Toggle visibility'
size='sm'
icon={<IoSunny size='18px' />}
colorScheme='blue'
variant={visible ? 'solid' : 'outline'}
onClick={() => actionHandler('update', { field: 'isPublic', value: !visible })}
/>
</Tooltip>
</div>
</>
);
}
InputRow.propTypes = {
label: PropTypes.string,
placeholder: PropTypes.string,
text: PropTypes.string,
visible: PropTypes.bool,
actionHandler: PropTypes.func.isRequired,
changeHandler: PropTypes.func.isRequired,
};
@@ -1,7 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import InputRow from './InputRow';
import OnAirIconBtn from '../../../common/components/buttons/OnAirIconBtn';
import { Tooltip } from '@chakra-ui/tooltip';
import { IconButton } from '@chakra-ui/button';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import style from './MessageControl.module.scss';
export default function MessageControl() {
@@ -57,44 +60,47 @@ export default function MessageControl() {
};
}, [socket]);
const messageControl = async (action, payload) => {
switch (action) {
case 'pres-text':
socket.emit('set-timer-text', payload);
break;
case 'toggle-pres-visible':
socket.emit('set-timer-visible', !pres.visible);
break;
case 'publ-text':
socket.emit('set-public-text', payload);
break;
case 'toggle-publ-visible':
socket.emit('set-public-visible', !publ.visible);
break;
case 'lower-text':
socket.emit('set-lower-text', payload);
break;
case 'toggle-lower-visible':
socket.emit('set-lower-visible', !lower.visible);
break;
case 'toggle-onAir':
socket.emit('set-onAir', !onAir);
break;
default:
break;
}
};
const messageControl = useCallback(
(action, payload) => {
switch (action) {
case 'pres-text':
socket.emit('set-timer-text', payload);
break;
case 'toggle-pres-visible':
socket.emit('set-timer-visible', payload);
break;
case 'publ-text':
socket.emit('set-public-text', payload);
break;
case 'toggle-publ-visible':
socket.emit('set-public-visible', payload);
break;
case 'lower-text':
socket.emit('set-lower-text', payload);
break;
case 'toggle-lower-visible':
socket.emit('set-lower-visible', payload);
break;
case 'toggle-onAir':
socket.emit('set-onAir', payload);
break;
default:
break;
}
},
[socket]
);
return (
<>
<div className={style.messageContainer}>
<InputRow
label='Presenter screen message'
label='Timer screen message'
placeholder='only the presenter screens see this'
text={pres.text}
visible={pres.visible}
changeHandler={(event) => messageControl('pres-text', event)}
actionHandler={() => messageControl('toggle-pres-visible')}
actionHandler={() => messageControl('toggle-pres-visible', !pres.visible)}
/>
<InputRow
label='Public screen message'
@@ -102,7 +108,7 @@ export default function MessageControl() {
text={publ.text}
visible={publ.visible}
changeHandler={(event) => messageControl('publ-text', event)}
actionHandler={() => messageControl('toggle-publ-visible')}
actionHandler={() => messageControl('toggle-publ-visible', !publ.visible)}
/>
<InputRow
label='Lower third message'
@@ -110,20 +116,23 @@ export default function MessageControl() {
text={lower.text}
visible={lower.visible}
changeHandler={(event) => messageControl('lower-text', event)}
actionHandler={() => messageControl('toggle-lower-visible')}
actionHandler={() => messageControl('toggle-lower-visible', !lower.visible)}
/>
</div>
<div className={style.onAirToggle}>
<OnAirIconBtn
className={style.btn}
active={onAir}
size='md'
actionHandler={() => messageControl('toggle-onAir')}
/>
<Tooltip label={onAir ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
<IconButton
className={style.btn}
size='md'
icon={onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
colorScheme='blue'
variant={onAir ? 'solid' : 'outline'}
onClick={() => messageControl('toggle-onAir', !onAir)}
aria-label='Toggle On Air'
/>
</Tooltip>
<span className={style.onAirLabel}>On Air</span>
<span className={style.oscLabel}>
{`/ontime/offAir << OSC >> /ontime/onAir`}
</span>
<span className={style.oscLabel}>{`/ontime/offAir << OSC >> /ontime/onAir`}</span>
</div>
</>
);
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import style from './PlaybackControl.module.scss';
import { useSocket } from 'app/context/socketContext';
import PlaybackButtons from './PlaybackButtons';
@@ -63,37 +63,40 @@ export default function PlaybackControl() {
};
}, [socket]);
const playbackControl = async (action) => {
switch (action) {
case 'start':
socket.emit('set-playstate', 'start');
break;
case 'pause':
socket.emit('set-playstate', 'pause');
break;
case 'roll':
socket.emit('set-playstate', 'roll');
break;
case 'previous':
socket.emit('set-playstate', 'previous');
resetTimer();
break;
case 'next':
socket.emit('set-playstate', 'next');
resetTimer();
break;
case 'unload':
socket.emit('set-playstate', 'unload');
resetTimer();
break;
case 'reload':
socket.emit('set-playstate', 'reload');
resetTimer();
break;
default:
break;
}
};
const playbackControl = useCallback(
(action) => {
switch (action) {
case 'start':
socket.emit('set-playstate', 'start');
break;
case 'pause':
socket.emit('set-playstate', 'pause');
break;
case 'roll':
socket.emit('set-playstate', 'roll');
break;
case 'previous':
socket.emit('set-playstate', 'previous');
resetTimer();
break;
case 'next':
socket.emit('set-playstate', 'next');
resetTimer();
break;
case 'unload':
socket.emit('set-playstate', 'unload');
resetTimer();
break;
case 'reload':
socket.emit('set-playstate', 'reload');
resetTimer();
break;
default:
break;
}
},
[socket]
);
return (
<div className={style.mainContainer}>
@@ -21,9 +21,8 @@ const areEqual = (prevProps, nextProps) => {
const incrementProps = {
size: 'sm',
width: '2.9em',
colorScheme: 'whiteAlpha',
colorScheme: 'white',
variant: 'outline',
_focus: { boxShadow: 'none' },
};
const PlaybackTimer = (props) => {
@@ -68,23 +67,43 @@ const PlaybackTimer = (props) => {
</>
)}
<div className={style.btn}>
<Tooltip label='Remove 1 minute' delay={500} shouldWrapChildren={disableButtons}>
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(-1)}>
<Tooltip label='Remove 1 minute' openDelay={500} shouldWrapChildren={disableButtons}>
<Button
{...incrementProps}
disabled={disableButtons}
onClick={() => handleIncrement(-1)}
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
>
-1
</Button>
</Tooltip>
<Tooltip label='Add 1 minute' delay={500} shouldWrapChildren={disableButtons}>
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(1)}>
<Tooltip label='Add 1 minute' openDelay={500} shouldWrapChildren={disableButtons}>
<Button
{...incrementProps}
disabled={disableButtons}
onClick={() => handleIncrement(1)}
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
>
+1
</Button>
</Tooltip>
<Tooltip label='Remove 5 minutes' delay={500} shouldWrapChildren={disableButtons}>
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(-5)}>
<Tooltip label='Remove 5 minutes' openDelay={500} shouldWrapChildren={disableButtons}>
<Button
{...incrementProps}
disabled={disableButtons}
onClick={() => handleIncrement(-5)}
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
>
-5
</Button>
</Tooltip>
<Tooltip label='Add 5 minutes' delay={500} shouldWrapChildren={disableButtons}>
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(5)}>
<Tooltip label='Add 5 minutes' openDelay={500} shouldWrapChildren={disableButtons}>
<Button
{...incrementProps}
disabled={disableButtons}
onClick={() => handleIncrement(5)}
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
>
+5
</Button>
</Tooltip>
@@ -1,10 +1,11 @@
import React from 'react';
import PrevIconBtn from '../../../common/components/buttons/PrevIconBtn';
import NextIconBtn from '../../../common/components/buttons/NextIconBtn';
import ReloadIconButton from '../../../common/components/buttons/ReloadIconBtn';
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
import UnloadIconBtn from '../../../common/components/buttons/UnloadIconBtn';
import PropTypes from 'prop-types';
import style from './PlaybackControl.module.scss';
import TransportIconBtn from '../../../common/components/buttons/TransportIconBtn';
export default function Transport(props) {
const { playback, selectedId, playbackControl, noEvents } = props;
@@ -12,25 +13,31 @@ export default function Transport(props) {
return (
<div className={style.playbackContainer}>
<PrevIconBtn
clickhandler={() => playbackControl('previous')}
<TransportIconBtn
clickHandler={() => playbackControl('previous')}
disabled={isRolling || noEvents}
tooltip='Previous event'
icon={<IoPlaySkipBack size='22px' />}
/>
<NextIconBtn
clickhandler={() => playbackControl('next')}
<TransportIconBtn
clickHandler={() => playbackControl('next')}
disabled={isRolling || noEvents}
tooltip='Next event'
icon={<IoPlaySkipForward size='22px' />}
/>
<ReloadIconButton
clickhandler={() => playbackControl('reload')}
<TransportIconBtn
clickHandler={() => playbackControl('reload')}
disabled={selectedId == null || isRolling || noEvents}
tooltip='Reload event'
icon={<IoArrowUndo size='22px' />}
/>
<UnloadIconBtn
clickhandler={() => playbackControl('unload')}
clickHandler={() => playbackControl('unload')}
disabled={(selectedId == null && !isRolling) || noEvents}
/>
</div>
);
};
}
Transport.propTypes = {
playback: PropTypes.string,
@@ -1,11 +1,12 @@
import React from 'react';
import { HStack } from '@chakra-ui/react';
import { Draggable } from 'react-beautiful-dnd';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import ActionButtons from '../list/ActionButtons';
import style from './BlockBlock.module.scss';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
import PropTypes from 'prop-types';
import style from './BlockBlock.module.scss';
export default function BlockBlock(props) {
const { index, data, actionHandler } = props;
@@ -22,7 +23,13 @@ export default function BlockBlock(props) {
<FiMoreVertical />
</span>
<HStack spacing='0.5em' className={style.actionOverlay}>
<DeleteIconBtn actionHandler={actionHandler} />
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
icon={<IoRemove />}
colorScheme='red'
tooltip='Delete'
_hover={{ bg: 'red.400' }}
/>
<ActionButtons showAdd showDelay actionHandler={actionHandler} />
</HStack>
</div>
@@ -1,21 +1,23 @@
import React from 'react';
import React, { useCallback } from 'react';
import { HStack } from '@chakra-ui/react';
import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { millisToMinutes } from 'common/utils/dateConfig';
import ActionButtons from '../list/ActionButtons';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import ApplyIconBtn from 'common/components/buttons/ApplyIconBtn';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import DelayInput from 'common/input/DelayInput';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
import style from './DelayBlock.module.scss';
import PropTypes from 'prop-types';
export default function DelayBlock(props) {
const { eventsHandler, data, index, actionHandler } = props;
const applyDelayHandler = () => {
const applyDelayHandler = useCallback(() => {
eventsHandler('applyDelay', { id: data.id, duration: data.duration });
};
}, [data.duration, data.id, eventsHandler]);
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
return (
@@ -27,8 +29,20 @@ export default function DelayBlock(props) {
</span>
<DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} />
<HStack spacing='0.5em' className={style.actionOverlay}>
<ApplyIconBtn clickhandler={applyDelayHandler} />
<DeleteIconBtn actionHandler={actionHandler} />
<TooltipActionBtn
clickHandler={applyDelayHandler}
icon={<FiCheck />}
colorScheme='orange'
tooltip='Apply delays'
_hover={{ bg: 'orange.400' }}
/>
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
icon={<IoRemove />}
colorScheme='red'
tooltip='Delete'
_hover={{ bg: 'red.400' }}
/>
<ActionButtons showAdd actionHandler={actionHandler} />
</HStack>
</div>
+3 -5
View File
@@ -41,11 +41,9 @@ export default function Editor() {
<Box className={styles.editor}>
<h1>Event List</h1>
<div className={styles.content}>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</div>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</Box>
</CollapseProvider>
</CursorProvider>
@@ -1,11 +1,19 @@
import React, { useContext, useEffect, useState } from 'react';
import { Tooltip } from '@chakra-ui/tooltip';
import { Checkbox } from '@chakra-ui/react';
import style from './EntryBlock.module.scss';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
import PropTypes from 'prop-types';
import style from './EntryBlock.module.scss';
export default function EntryBlock(props) {
const { showKbd, index, eventsHandler } = props;
const {
showKbd,
previousId,
eventsHandler,
visible,
disableAddDelay = true,
disableAddBlock,
} = props;
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
const [doPublic, setPublic] = useState(defaultPublic);
@@ -19,33 +27,36 @@ export default function EntryBlock(props) {
}, [defaultPublic]);
return (
<div className={style.create}>
<div className={`${style.create} ${visible ? style.visible : ''}`}>
<Tooltip label='Add Event' openDelay={300}>
<span
className={style.createEvent}
onClick={() =>
eventsHandler(
'add',
{ type: 'event', order: index + 1, isPublic: doPublic },
{ startIsLastEnd: doStartTime ? index : undefined }
{ type: 'event', after: previousId, isPublic: doPublic },
{ startIsLastEnd: doStartTime ? previousId : undefined }
)
}
role='button'
>
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
</span>
</Tooltip>
<Tooltip label='Add Delay' openDelay={300}>
<span
className={style.createDelay}
onClick={() => eventsHandler('add', { type: 'delay', order: index + 1 })}
className={`${style.createDelay} ${disableAddDelay ? style.disabled : ''}`}
onClick={() => eventsHandler('add', { type: 'delay', after: previousId })}
role='button'
>
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
</span>
</Tooltip>
<Tooltip label='Add Block' openDelay={300}>
<span
className={style.createBlock}
onClick={() => eventsHandler('add', { type: 'block', order: index + 1 })}
className={`${style.createBlock} ${disableAddBlock ? style.disabled : ''}`}
onClick={() => eventsHandler('add', { type: 'block', after: previousId })}
role='button'
>
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
</span>
@@ -65,9 +76,19 @@ export default function EntryBlock(props) {
isChecked={doPublic}
onChange={(e) => setPublic(e.target.checked)}
>
Default public
Event is public
</Checkbox>
</div>
</div>
);
}
EntryBlock.propTypes = {
showKbd: PropTypes.bool,
eventsHandler: PropTypes.func,
visible: PropTypes.bool,
previousId: PropTypes.string,
disableAddDelay: PropTypes.bool,
disableAddBlock: PropTypes.bool,
};
@@ -1,5 +1,20 @@
@use '../../../styles/main' as *;
@mixin when-visible() {
opacity: 1;
height: calc(2.5em + 20px);
display: flex;
align-items: center;
justify-content: center;
gap: 5%;
transition: height 0.15s ease;
margin: 2px 0;
* {
display: flex;
}
}
.create {
padding: 0 0.5em;
box-sizing: border-box;
@@ -18,17 +33,7 @@
}
&:hover {
opacity: 1;
height: calc(2.5em + 20px);
display: flex;
align-items: center;
justify-content: center;
gap: 5%;
transition: height 0.15s ease;
* {
display: flex;
}
@include when-visible;
}
.createEvent,
@@ -43,6 +48,7 @@
line-height: 21px;
border-radius: 4px;
opacity: 0.6;
cursor: pointer;
.keyboard {
margin-left: 4px;
@@ -85,8 +91,17 @@
}
.options {
display: flex;
flex-direction: column;
opacity: 0.65;
}
.disabled {
border-color: $text-gray-disabled;
color: $text-gray-disabled;
pointer-events: none;
}
}
.create.visible {
@include when-visible;
}
@@ -1,14 +1,25 @@
import React from 'react';
import React, { memo } from 'react';
import { HStack } from '@chakra-ui/react';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import EventTimes from '../../../common/components/eventTimes/EventTimes';
import EditableText from '../../../common/input/EditableText';
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
import ActionButtons from '../list/ActionButtons';
import PropTypes from 'prop-types';
import style from './EventBlock.module.scss';
export default function CollapsedBlock (props) {
const areEqual = (prevProps, nextProps) => {
return (
prevProps.provided === nextProps.provided &&
prevProps.data.revision === nextProps.data.revision &&
prevProps.next === nextProps.next &&
prevProps.delay === nextProps.delay &&
prevProps.delayValue === nextProps.delayValue &&
prevProps.previousEnd === nextProps.previousEnd
);
};
function CollapsedBlock(props) {
const { provided, data, next, delay, delayValue, previousEnd, actionHandler } = props;
return (
@@ -43,7 +54,7 @@ export default function CollapsedBlock (props) {
</HStack>
</>
);
};
}
CollapsedBlock.propTypes = {
provided: PropTypes.any.isRequired,
@@ -54,3 +65,5 @@ CollapsedBlock.propTypes = {
previousEnd: PropTypes.number.isRequired,
actionHandler: PropTypes.func.isRequired,
};
export default memo(CollapsedBlock, areEqual);
@@ -1,15 +1,28 @@
import React from 'react';
import React, { memo } from 'react';
import { VStack } from '@chakra-ui/react';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import EventTimesVertical from '../../../common/components/eventTimes/EventTimesVertical';
import EditableText from '../../../common/input/EditableText';
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
import ActionButtons from '../list/ActionButtons';
import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn';
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
import PropTypes from 'prop-types';
import style from './EventBlock.module.scss';
export default function ExpandedBlock(props) {
const areEqual = (prevProps, nextProps) => {
return (
prevProps.provided === nextProps.provided &&
prevProps.data.revision === nextProps.data.revision &&
prevProps.next === nextProps.next &&
prevProps.delay === nextProps.delay &&
prevProps.delayValue === nextProps.delayValue &&
prevProps.previousEnd === nextProps.previousEnd
);
};
function ExpandedBlock(props) {
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
const oscid = data?.id || '...';
@@ -70,11 +83,17 @@ export default function ExpandedBlock(props) {
<VStack spacing='0.5em' className={style.actionOverlay}>
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
<DeleteIconBtn actionHandler={actionHandler} />
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
icon={<IoRemove />}
colorScheme='red'
tooltip='Delete'
_hover={{ bg: 'red.400' }}
/>
</VStack>
</>
);
};
}
ExpandedBlock.propTypes = {
provided: PropTypes.any.isRequired,
@@ -86,3 +105,5 @@ ExpandedBlock.propTypes = {
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
};
export default memo(ExpandedBlock, areEqual);
@@ -0,0 +1,11 @@
import React from 'react';
import ProtectRoute from '../../common/components/protectRoute/ProtectRoute';
import Editor from './Editor';
export default function ProtectedEditor() {
return (
<ProtectRoute>
<Editor />
</ProtectRoute>
);
}
@@ -1,49 +0,0 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { Tooltip } from '@chakra-ui/tooltip';
export default function ActionButtons(props) {
const { showAdd, showDelay, showBlock, actionHandler } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...' delay={500}>
<MenuButton
as={IconButton}
aria-label='Options'
size='xs'
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
backgroundColor='orange.200'
color='orange.500'
/>
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
Add Event after
</MenuItem>
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')} isDisabled={!showDelay}>
Add Delay after
</MenuItem>
<MenuItem
icon={<FiMinusCircle />}
onClick={() => actionHandler('block')}
isDisabled={!showBlock}
>
Add Block after
</MenuItem>
</MenuList>
</Menu>
);
}
+44 -23
View File
@@ -18,6 +18,20 @@ export default function EventList(props) {
const cursorRef = createRef();
const { showQuickEntry } = useContext(LocalEventSettingsContext);
const insertAtCursor = useCallback((type, cursor) => {
if (cursor === -1) {
eventsHandler('add', { type: type });
} else {
const previousEvent = events[cursor];
const nextEvent = events[cursor + 1];
if (type === 'event') {
eventsHandler('add', { type: type, after: previousEvent.id });
} else if (previousEvent?.type !== type && nextEvent?.type !== type) {
eventsHandler('add', { type: type, after: previousEvent.id });
}
}
},[events, eventsHandler])
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
(e) => {
@@ -37,23 +51,23 @@ export default function EventList(props) {
if (e.key === 'e' || e.key === 'E') {
e.preventDefault();
if (cursor == null) return;
eventsHandler('add', { type: 'event', order: cursor + 1 });
insertAtCursor('event', cursor)
}
// D
if (e.key === 'd' || e.key === 'D') {
e.preventDefault();
if (cursor == null) return;
eventsHandler('add', { type: 'delay', order: cursor + 1 });
insertAtCursor('delay', cursor)
}
// B
if (e.key === 'b' || e.key === 'B') {
e.preventDefault();
if (cursor == null) return;
eventsHandler('add', { type: 'block', order: cursor + 1 });
insertAtCursor('block', cursor)
}
}
},
[cursor, events.length, eventsHandler, moveCursorDown, moveCursorUp]
[cursor, events.length, insertAtCursor, moveCursorDown, moveCursorUp]
);
useEffect(() => {
@@ -61,6 +75,7 @@ export default function EventList(props) {
document.addEventListener('keydown', handleKeyPress);
if (cursor > events.length - 1) setCursor(events.length - 1);
if (events.length > 0 && cursor === -1) setCursor(0);
// remove the event listener
return () => {
@@ -126,26 +141,28 @@ export default function EventList(props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedId, isCursorLocked]);
if (events.length < 1) {
return <Empty text='No Events' style={{marginTop: "10vh"}} />;
}
// DND
const handleOnDragEnd = (result) => {
// drop outside of area
if (!result.destination) return;
const handleOnDragEnd = useCallback(
(result) => {
// drop outside of area
if (!result.destination) return;
// no change
if (result.destination === result.source.index) return;
// no change
if (result.destination === result.source.index) return;
// Call API
eventsHandler('reorder', {
index: result.draggableId,
from: result.source.index,
to: result.destination.index,
});
};
// Call API
eventsHandler('reorder', {
index: result.draggableId,
from: result.source.index,
to: result.destination.index,
});
},
[eventsHandler]
);
if (events.length < 1) {
return <Empty text='No Events' style={{ marginTop: '10vh' }} />;
}
let cumulativeDelay = 0;
let eventIndex = -1;
let previousEnd = 0;
@@ -171,10 +188,11 @@ export default function EventList(props) {
previousEnd = thisEnd;
thisEnd = e.timeEnd;
}
const isLast = index === events.length - 1;
return (
<div key={e.id}>
{index === 0 && showQuickEntry && (
<EntryBlock index={-1} eventsHandler={eventsHandler} />
<EntryBlock index={e.id} eventsHandler={eventsHandler} />
)}
<div
ref={cursor === index ? cursorRef : undefined}
@@ -192,11 +210,14 @@ export default function EventList(props) {
previousEnd={previousEnd}
/>
</div>
{showQuickEntry && (
{(showQuickEntry || isLast) && (
<EntryBlock
showKbd={index === cursor}
index={index}
previousId={e.id}
eventsHandler={eventsHandler}
visible={isLast}
disableAddDelay={e.type === 'delay'}
disableAddBlock={e.type === 'block'}
/>
)}
</div>
@@ -4,6 +4,7 @@ import BlockBlock from '../BlockBlock/BlockBlock';
import EventBlock from '../EventBlock/EventBlock';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
import PropTypes from 'prop-types';
const areEqual = (prevProps, nextProps) => {
return (
@@ -51,17 +52,17 @@ const EventListItem = (props) => {
'add',
{
type: 'event',
order: index + 1,
after: data.id,
isPublic: defaultPublic,
},
{ startIsLastEnd: starTimeIsLastEnd ? index : undefined }
{ startIsLastEnd: starTimeIsLastEnd ? data.id : undefined }
);
break;
case 'delay':
eventsHandler('add', { type: 'delay', order: index + 1 });
eventsHandler('add', { type: 'delay', after: data.id });
break;
case 'block':
eventsHandler('add', { type: 'block', order: index + 1 });
eventsHandler('add', { type: 'block', after: data.id });
break;
case 'delete':
eventsHandler('delete', data.id);
@@ -101,7 +102,7 @@ const EventListItem = (props) => {
break;
}
},
[calculateDuration, data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd]
[calculateDuration, data, defaultPublic, emitError, eventsHandler, starTimeIsLastEnd]
);
switch (type) {
@@ -135,3 +136,15 @@ const EventListItem = (props) => {
};
export default memo(EventListItem, areEqual);
EventListItem.propTypes = {
type: PropTypes.oneOf(['event', 'delay', 'block']),
index: PropTypes.number,
eventIndex: PropTypes.number,
data: PropTypes.object,
selected: PropTypes.bool,
next: PropTypes.bool,
eventsHandler: PropTypes.func,
delay: PropTypes.number,
previousEnd: PropTypes.number
}
@@ -17,6 +17,7 @@ import EventList from './EventList';
import EventListMenu from 'features/menu/EventListMenu.jsx';
import Empty from 'common/state/Empty';
import { CollapseContext } from '../../../app/context/CollapseContext';
import styles from '../Editor.module.scss';
export default function EventListWrapper() {
const { expandAll, collapseMultiple } = useContext(CollapseContext);
@@ -40,7 +41,17 @@ export default function EventListWrapper() {
// optimistically update object, temp ID until refetch
const optimistic = [...previousEvents];
optimistic.splice(newEvent.order, 0, {
let insertAfterIndex = 0;
if (newEvent.after) {
const index = optimistic.findIndex((event) => event.id === newEvent?.after);
if (index > -1) {
insertAfterIndex = index + 1;
}
} else if (newEvent.order) {
insertAfterIndex = newEvent.order;
}
optimistic.splice(insertAfterIndex, 0, {
...newEvent,
id: new Date().toISOString(),
});
@@ -50,7 +61,7 @@ export default function EventListWrapper() {
return { previousEvents };
},
// Mutation fails, rollback undos optimist update
// Mutation fails, rollback undoes optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
},
@@ -128,7 +139,7 @@ export default function EventListWrapper() {
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
const filtered = [...previousEvents].filter((e) => e.id === 'eventId')
const filtered = [...previousEvents].filter((e) => e.id !== eventId);
// optimistically update object
queryClient.setQueryData(EVENTS_TABLE, filtered);
@@ -231,12 +242,16 @@ export default function EventListWrapper() {
const newEvent = { ...payload };
// there is an option to pass an index of an array to use as start time
if (typeof options?.startIsLastEnd !== 'undefined') {
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
const previousEvent = data.find((event) => event.id === options.startIsLastEnd);
newEvent.timeStart = previousEvent.timeEnd || 0;
}
// hard coding duration value to be as expected for now
// this until timeOptions gets implemented
// Todo: implement duration options
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
if (newEvent.type === 'event') {
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
}
await addEvent.mutateAsync(newEvent);
} catch (error) {
emitError(`Error fetching data: ${error.message}`);
@@ -336,11 +351,13 @@ export default function EventListWrapper() {
return (
<>
<EventListMenu eventsHandler={eventsHandler} />
{status === 'success' && events != null ? (
<EventList events={events} eventsHandler={eventsHandler} />
) : (
<Empty text='Connecting to server' />
)}
<div className={styles.content}>
{status === 'success' && events != null ? (
<EventList events={events} eventsHandler={eventsHandler} />
) : (
<Empty text='Connecting to server' />
)}
</div>
</>
);
}
+20 -8
View File
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { HStack } from '@chakra-ui/react';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import { LoggingContext } from '../../app/context/LoggingContext';
@@ -46,14 +46,14 @@ export default function InfoLogger() {
setData(d);
}, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]);
const disableOthers = (toEnable) => {
const disableOthers = useCallback((toEnable) => {
toEnable === 'USER' ? setShowUser(true) : setShowUser(false);
toEnable === 'CLIENT' ? setShowClient(true) : setShowClient(false);
toEnable === 'SERVER' ? setShowServer(true) : setShowServer(false);
toEnable === 'RX' ? setShowRx(true) : setShowRx(false);
toEnable === 'TX' ? setShowTx(true) : setShowTx(false);
toEnable === 'PLAYBACK' ? setShowPlayback(true) : setShowPlayback(false);
};
}, []);
return (
<div className={collapsed ? style.container : style.container__expanded}>
@@ -64,46 +64,58 @@ export default function InfoLogger() {
<div
onClick={() => setShowUser((s) => !s)}
onAuxClick={() => disableOthers('USER')}
onContextMenu={(e) => e.preventDefault()}
className={showUser ? style.active : null}
role='button'
>
USER
</div>
<div
onClick={() => setShowClient((s) => !s)}
onAuxClick={() => disableOthers('CLIENT')}
onContextMenu={(e) => e.preventDefault()}
className={showClient ? style.active : null}
role='button'
>
CLIENT
</div>
<div
onClick={() => setShowServer((s) => !s)}
onAuxClick={() => disableOthers('SERVER')}
onContextMenu={(e) => e.preventDefault()}
className={showServer ? style.active : null}
role='button'
>
SERVER
</div>
<div
onClick={() => setShowPlayback((s) => !s)}
onAuxClick={() => disableOthers('PLAYBACK')}
onContextMenu={(e) => e.preventDefault()}
className={showPlayback ? style.active : null}
role='button'
>
Playback
</div>
<div
onClick={() => setShowRx((s) => !s)}
onAuxClick={() => disableOthers('RX')}
onContextMenu={(e) => e.preventDefault()}
className={showRx ? style.active : null}
role='button'
>
RX
</div>
<div
onClick={() => setShowTx((s) => !s)}
onAuxClick={() => disableOthers('TX')}
onContextMenu={(e) => e.preventDefault()}
className={showTx ? style.active : null}
role='button'
>
TX
</div>
<div onClick={clearLog} className={style.clear}>
<div onClick={clearLog} className={style.clear} role='button'>
Clear
</div>
</HStack>
@@ -115,10 +127,10 @@ export default function InfoLogger() {
d.level === 'INFO'
? style.info
: d.level === 'WARN'
? style.warn
: d.level === 'ERROR'
? style.error
: ''
? style.warn
: d.level === 'ERROR'
? style.error
: ''
}
>
<div className={style.time}>{d.time}</div>
+8 -1
View File
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import style from './Info.module.scss';
import PropTypes from 'prop-types';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import style from './Info.module.scss';
export default function InfoTitle(props) {
const [collapsed, setCollapsed] = useState(false);
@@ -43,3 +44,9 @@ export default function InfoTitle(props) {
</div>
);
}
InfoTitle.propTypes = {
title: PropTypes.string,
data: PropTypes.object,
roll: PropTypes.bool,
}
@@ -1,3 +1,4 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import SocketProvider from 'app/context/socketContext';
import { QueryClient, QueryClientProvider } from 'react-query';
+93 -46
View File
@@ -1,67 +1,114 @@
import React, { memo, useContext } from 'react';
import { ButtonGroup, Divider, HStack } from '@chakra-ui/react';
import React, { memo, useCallback, useContext } from 'react';
import { ButtonGroup, HStack } from '@chakra-ui/react';
import { CursorContext } from '../../app/context/CursorContext';
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import MenuActionButtons from './MenuActionButtons';
import CollapseBtn from 'common/components/buttons/CollapseBtn';
import CursorUpBtn from '../../common/components/buttons/CursorUpBtn';
import CursorDownBtn from '../../common/components/buttons/CursorDownBtn';
import CursorLockedBtn from 'common/components/buttons/CursorLockedBtn';
import PropTypes from 'prop-types';
import style from './EventListMenu.module.css';
import ExpandBtn from '../../common/components/buttons/ExpandBtn';
const EventListMenu = ({ eventsHandler }) => {
const { isCursorLocked, toggleCursorLocked, moveCursorUp, moveCursorDown } =
useContext(CursorContext);
const actionHandler = (action) => {
switch (action) {
case 'event':
eventsHandler('add', { type: action, order: 0 });
break;
case 'delay':
eventsHandler('add', { type: action, order: 0 });
break;
case 'block':
eventsHandler('add', { type: action, order: 0 });
break;
case 'cursorUp':
moveCursorUp();
break;
case 'cursorDown':
moveCursorDown();
break;
case 'togglelock':
toggleCursorLocked();
break;
case 'deleteall':
eventsHandler('deleteall');
break;
default:
break;
}
const actionHandler = useCallback(
(action) => {
switch (action) {
case 'event':
eventsHandler('add', { type: action });
break;
case 'delay':
eventsHandler('add', { type: action });
break;
case 'block':
eventsHandler('add', { type: action });
break;
case 'cursorUp':
moveCursorUp();
break;
case 'cursorDown':
moveCursorDown();
break;
case 'togglelock':
toggleCursorLocked();
break;
case 'deleteall':
eventsHandler('deleteall');
break;
default:
break;
}
},
[eventsHandler, moveCursorDown, moveCursorUp, toggleCursorLocked]
);
const collapsingBtnProps = {
variant: 'outline',
size: 'sm',
};
const cursorBtnProps = {
size: 'sm',
color: 'pink.300',
borderColor: 'pink.300',
variant: 'outline',
};
return (
<HStack className={style.headerButtons}>
<ButtonGroup isAttached>
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
</ButtonGroup>
<Divider orientation='vertical' />
<ButtonGroup isAttached>
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
<CursorLockedBtn
size='sm'
clickhandler={() => actionHandler('togglelock')}
active={isCursorLocked}
width='3em'
<TooltipActionBtn
clickHandler={() => eventsHandler('expandall')}
icon={<FiChevronsDown />}
tooltip='Expand All'
_hover={{ bg: '#ebedf0', color: '#333' }}
{...collapsingBtnProps}
/>
<TooltipActionBtn
clickHandler={() => eventsHandler('collapseall')}
icon={<FiChevronsUp />}
tooltip='Collapse All'
_hover={{ bg: '#ebedf0', color: '#333' }}
{...collapsingBtnProps}
/>
</ButtonGroup>
<ButtonGroup isAttached>
<TooltipActionBtn
{...cursorBtnProps}
clickHandler={() => actionHandler('cursorUp')}
icon={<IoCaretUp />}
tooltip='Move cursor up Alt + ↑'
_hover={{ bg: 'pink.400' }}
/>
<TooltipActionBtn
{...cursorBtnProps}
clickHandler={() => actionHandler('cursorDown')}
icon={<IoCaretDown />}
tooltip='Move cursor down Alt + ↓'
_hover={{ bg: 'pink.400' }}
/>
<TooltipActionBtn
{...cursorBtnProps}
clickHandler={() => actionHandler('togglelock')}
icon={<FiTarget />}
tooltip='Lock cursor to current'
width='3em'
backgroundColor={isCursorLocked && 'pink.400'}
_hover={{ bg: 'pink.300' }}
variant={isCursorLocked ? 'solid' : 'outline'}
/>
</ButtonGroup>
<Divider orientation='vertical' />
<MenuActionButtons actionHandler={actionHandler} size='sm' />
</HStack>
);
};
export default memo(EventListMenu);
EventListMenu.propTypes = {
eventsHandler: PropTypes.func,
};
@@ -1,4 +1,5 @@
.headerButtons {
align-content: center;
justify-content: flex-end;
gap: 8px;
}
@@ -7,9 +7,10 @@ import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
import { Divider } from '@chakra-ui/layout';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
export default function MenuActionButtons(props) {
const { actionHandler, size } = props;
const { actionHandler, size = 'xs' } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
@@ -21,7 +22,7 @@ export default function MenuActionButtons(props) {
<MenuButton
as={IconButton}
aria-label='Create Menu'
size={size || 'xs'}
size={size}
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
@@ -47,3 +48,8 @@ export default function MenuActionButtons(props) {
</Menu>
);
}
MenuActionButtons.propTypes = {
actionHandler: PropTypes.func,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
};
+79 -51
View File
@@ -1,18 +1,19 @@
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
import { EVENTS_TABLE } from 'app/api/apiConstants';
import DownloadIconBtn from './buttons/DownloadIconBtn';
import SettingsIconBtn from './buttons/SettingsIconBtn';
import MaxIconBtn from './buttons/MaxIconBtn';
import MinIconBtn from './buttons/MinIconBtn';
import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.scss';
import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
import { VStack } from '@chakra-ui/react';
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
import { EVENTS_TABLE } from 'app/api/apiConstants';
import { LoggingContext } from '../../app/context/LoggingContext';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
import PropTypes from 'prop-types';
import style from './MenuBar.module.scss';
export default function MenuBar(props) {
const { isOpen, onOpen, onClose } = props;
@@ -25,46 +26,47 @@ export default function MenuBar(props) {
},
});
const handleDownload = () => {
downloadEvents();
};
const handleClick = () => {
const handleClick = useCallback(() => {
if (hiddenFileInput && hiddenFileInput.current) {
hiddenFileInput.current.click();
}
};
}, [hiddenFileInput]);
const buttonStyle = {
fontSize: '1.5em',
size: 'lg',
colorScheme: 'white',
};
const handleUpload = (event) => {
const fileUploaded = event.target.files[0];
if (fileUploaded == null) return;
const handleUpload = useCallback(
(event) => {
const fileUploaded = event.target.files[0];
if (fileUploaded == null) return;
// Limit file size to 1MB
if (fileUploaded.size > 1000000) {
emitError('Error: File size limit (1MB) exceeded');
return;
}
// Check file extension
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
try {
uploaddb.mutate(fileUploaded);
} catch (error) {
emitError(`Failed uploading file: ${error}`);
// Limit file size to 1MB
if (fileUploaded.size > 1000000) {
emitError('Error: File size limit (1MB) exceeded');
return;
}
} else {
emitError('Error: File type unknown');
}
// reset input value
hiddenFileInput.current.value = '';
};
// Check file extension
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
try {
uploaddb.mutate(fileUploaded);
} catch (error) {
emitError(`Failed uploading file: ${error}`);
}
} else {
emitError('Error: File type unknown');
}
const handleIPC = (action) => {
// reset input value
hiddenFileInput.current.value = '';
},
[emitError, uploaddb]
);
const handleIPC = useCallback((action) => {
// Stop crashes when testing locally
if (typeof window.process?.type === 'undefined') {
if (action === 'help') {
@@ -91,7 +93,7 @@ export default function MenuBar(props) {
break;
}
}
};
}, []);
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
@@ -125,16 +127,32 @@ export default function MenuBar(props) {
return (
<VStack>
<QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} />
<MaxIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('max')} />
<MinIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('min')} />
<QuitIconBtn clickHandler={() => handleIPC('shutdown')} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiMaximize />}
clickHandler={() => handleIPC('max')}
tooltip='Show full window'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiMinimize />}
clickHandler={() => handleIPC('min')}
tooltip='Close to tray'
/>
<div className={style.gap} />
<HelpIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('help')} />
<SettingsIconBtn
style={{ ...buttonStyle }}
size='lg'
<TooltipActionBtn
{...buttonStyle}
icon={<FiHelpCircle />}
clickHandler={() => handleIPC('help')}
tooltip='Help'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiSettings />}
className={isOpen ? style.open : ''}
clickhandler={onOpen}
clickHandler={onOpen}
tooltip='Settings'
isRound
/>
<div className={style.gap} />
@@ -145,8 +163,18 @@ export default function MenuBar(props) {
onChange={handleUpload}
accept='.json, .xlsx'
/>
<UploadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleClick} />
<DownloadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleDownload} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiUpload />}
clickHandler={handleClick}
tooltip='Import event list'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiDownload />}
clickHandler={downloadEvents}
tooltip='Export event list'
/>
</VStack>
);
}
@@ -1,3 +1,4 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import MenuBar from '../MenuBar';
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
@@ -1,3 +1,4 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import MenuActionButtons from "../MenuActionButtons";
@@ -1,3 +1,4 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import MenuBar from '../MenuBar';
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
@@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
export default function DownloadIconBtn(props) {
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Export event list'>
<IconButton
size={size || 'xs'}
icon={<FiDownload />}
colorScheme='white'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
export default function HelpIconBtn(props) {
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Help'>
<IconButton
size={size || 'xs'}
icon={<FiHelpCircle />}
colorScheme='white'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
export default function MaxIconBtn(props) {
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Show full window'>
<IconButton
size={size || 'xs'}
icon={<FiMaximize />}
colorScheme='white'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
export default function MinIconBtn(props) {
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Close to tray'>
<IconButton
size={size || 'xs'}
icon={<FiMinimize />}
colorScheme='white'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,68 +0,0 @@
import React, { useRef, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button';
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
} from '@chakra-ui/modal';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiPower } from '@react-icons/all-files/fi/FiPower';
export default function QuitIconBtn(props) {
const { clickhandler, size, ...rest } = props;
const [isOpen, setIsOpen] = useState(false);
const onClose = () => setIsOpen(false);
const cancelRef = useRef();
const handleShutdown = () => {
onClose();
clickhandler();
};
return (
<>
<Tooltip label='Quit Application'>
<IconButton
size={size || 'xs'}
icon={<FiPower />}
colorScheme='red'
variant='outline'
isRound
onClick={() => setIsOpen(true)}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Server Shutdown
</AlertDialogHeader>
<AlertDialogBody>
This will shutdown the program and all running servers. Are you
sure?
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button colorScheme='red' onClick={handleShutdown} ml={3}>
Shutdown
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
);
}
@@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
export default function SettingsIconBtn(props) {
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Settings'>
<IconButton
size={size || 'xs'}
icon={<FiSettings />}
colorScheme='white'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
export default function UploadIconBtn(props) {
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Import event list'>
<IconButton
size={size || 'xs'}
icon={<FiUpload />}
colorScheme='white'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
+76 -67
View File
@@ -1,4 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
/* eslint-disable jsx-a11y/anchor-has-content */
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
@@ -13,7 +14,7 @@ import { viewerLocations } from '../../app/appConstants';
import { LoggingContext } from '../../app/context/LoggingContext';
import { validateAlias } from '../../app/utils/aliases';
import SubmitContainer from './SubmitContainer';
import { handleLinks, host, openLink } from '../../common/utils/linkUtils';
import { handleLinks, host } from '../../common/utils/linkUtils';
import style from './Modals.module.scss';
export default function AliasesModal() {
@@ -35,45 +36,48 @@ export default function AliasesModal() {
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const submitHandler = useCallback(
async (event) => {
event.preventDefault();
setSubmitting(true);
const validatedAliases = [...aliases];
let errors = false;
for (const alias of validatedAliases) {
// validate url
const isURLValid = validateAlias(alias.pathAndParams);
if (!isURLValid.status) {
alias.urlError = isURLValid.message;
errors = true;
} else {
alias.urlError = undefined;
const validatedAliases = [...aliases];
let errors = false;
for (const alias of validatedAliases) {
// validate url
const isURLValid = validateAlias(alias.pathAndParams);
if (!isURLValid.status) {
alias.urlError = isURLValid.message;
errors = true;
} else {
alias.urlError = undefined;
}
// validate alias
const isAliasValid = validateAlias(alias.alias);
if (!isAliasValid.status) {
alias.aliasError = isAliasValid.message;
errors = true;
} else {
alias.aliasError = undefined;
}
}
// validate alias
const isAliasValid = validateAlias(alias.alias);
if (!isAliasValid.status) {
alias.aliasError = isAliasValid.message;
errors = true;
} else {
alias.aliasError = undefined;
setAliases(validatedAliases);
if (!errors) {
await postAliases(aliases);
await refetch();
setChanged(false);
}
}
setAliases(validatedAliases);
if (!errors) {
await postAliases(aliases);
await refetch();
setChanged(false);
}
setSubmitting(false);
};
setSubmitting(false);
},
[aliases, refetch]
);
/**
* Creates a new alias in state with a temporary id
*/
const addNew = () => {
const addNew = useCallback(() => {
if (aliases.length > 20) {
emitError('Maximum amount of aliases reacted (20)');
return;
@@ -87,23 +91,23 @@ export default function AliasesModal() {
};
setAliases((prevState) => [...prevState, emptyAlias]);
setChanged(true);
};
}, [aliases.length, emitError]);
/**
* Deletes an alias by a given id
* @param {string} id - id of alias to delete
*/
const deleteAlias = (id) => {
const deleteAlias = useCallback((id) => {
setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]);
setChanged(true);
};
}, []);
/**
* Sets enabled flag to true / false
* @param {string} id - object id
* @param {boolean} isEnabled - whether to enable / disable flag
*/
const setEnabled = (id, isEnabled) => {
const setEnabled = useCallback((id, isEnabled) => {
const aliasesState = [...aliases];
for (const a of aliasesState) {
if (a.id === id) {
@@ -125,7 +129,7 @@ export default function AliasesModal() {
}
setChanged(true);
setAliases(aliasesState);
};
}, [aliases, emitError]);
/**
* Reverts local state equals to server state
@@ -141,12 +145,15 @@ export default function AliasesModal() {
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (index, field, value) => {
const temp = [...aliases];
temp[index][field] = value;
setAliases(temp);
setChanged(true);
};
const handleChange = useCallback(
(index, field, value) => {
const temp = [...aliases];
temp[index][field] = value;
setAliases(temp);
setChanged(true);
},
[aliases]
);
return (
<ModalBody className={style.modalBody}>
@@ -183,16 +190,16 @@ export default function AliasesModal() {
eg. a lower third url with some custom parameters
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>mylower</td>
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
</tr>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>mylower</td>
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
</tr>
</tbody>
</table>
<br />
@@ -201,20 +208,20 @@ export default function AliasesModal() {
eg. an unattended screen that you would need to change route from the app
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>thirdfloor</td>
<td>public</td>
</tr>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>thirdfloor</td>
<td>public</td>
</tr>
</tbody>
</table>
</div>
<div className={style.inlineAliasPlaceholder} style={{ padding: '0.5em 0' }}>
<div className={style.inlineAliasPlaceholder}>
<span className={style.labelNote}>Alias</span>
<span className={style.labelNote}>Page URL</span>
</div>
@@ -247,11 +254,12 @@ export default function AliasesModal() {
href='#!'
target='_blank'
rel='noreferrer'
onClick={(e) => openLink(e, alias.pathAndParams)}
onClick={(e) => handleLinks(e, alias.pathAndParams)}
/>
</Tooltip>
<Tooltip label='Enable alias' openDelay={500}>
<IconButton
aria-label='Enable alias'
size='xs'
icon={<IoSunny />}
colorScheme='blue'
@@ -261,6 +269,7 @@ export default function AliasesModal() {
</Tooltip>
<Tooltip label='Delete alias' openDelay={500}>
<IconButton
aria-label='Delete alias'
size='xs'
icon={<IoRemove />}
colorScheme='red'
@@ -277,7 +286,7 @@ export default function AliasesModal() {
</div>
))}
<div className={style.inlineAliasPlaceholder} style={{ padding: '0.5em 0' }}>
<div className={style.inlineAliasPlaceholder}>
<Button size='xs' colorScheme='blue' variant='outline' onClick={() => addNew()}>
Add new
</Button>
+75 -43
View File
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { Checkbox, FormControl, FormLabel, Input, PinInput, PinInputField } from '@chakra-ui/react';
import { getSettings, ontimePlaceholderSettings, postSettings } from 'app/api/ontimeApi';
@@ -8,9 +8,12 @@ import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import { IconButton } from '@chakra-ui/button';
import { FiEye } from '@react-icons/all-files/fi/FiEye';
import { FiX } from '@react-icons/all-files/fi/FiX';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
import { LocalEventSettingsContext } from '../../app/context/LocalEventSettingsContext';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
const version = require('../../../package.json').version
export default function AppSettingsModal() {
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
@@ -65,48 +68,63 @@ export default function AppSettingsModal() {
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const submitHandler = useCallback(
async (event) => {
event.preventDefault();
setSubmitting(true);
// set context
setShowQuickEntry(doShowQuickEntry);
setStarTimeIsLastEnd(doStarTimeIsLastEnd);
setDefaultPublic(doDefaultPublic);
// set context
setShowQuickEntry(doShowQuickEntry);
setStarTimeIsLastEnd(doStarTimeIsLastEnd);
setDefaultPublic(doDefaultPublic);
const f = formData;
const f = formData;
// we might not have changed this
if (f.pinCode !== data.pinCode) {
const e = { status: false, message: '' };
// we might not have changed this
if (f.pinCode !== data.pinCode) {
const e = { status: false, message: '' };
// Validate fields
if (f.pinCode === '' || f.pinCode == null) {
e.status = true;
e.message += 'App pin code removed';
} else {
e.status = true;
e.message += 'App pin code added';
// Validate fields
if (f.pinCode === '' || f.pinCode == null) {
e.status = true;
e.message += 'App pin code removed';
} else {
e.status = true;
e.message += 'App pin code added';
}
// set fields with error
if (!e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
await postSettings(formData);
await refetch();
emitWarning(e.message);
setChanged(false);
}
}
// set fields with error
if (!e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
await postSettings(formData);
await refetch();
emitWarning(e.message);
setChanged(false);
}
}
setSubmitting(false);
setChanged(false);
};
setSubmitting(false);
setChanged(false);
},
[
data.pinCode,
doDefaultPublic,
doShowQuickEntry,
doStarTimeIsLastEnd,
emitError,
emitWarning,
formData,
refetch,
setDefaultPublic,
setShowQuickEntry,
setStarTimeIsLastEnd,
]
);
/**
* Reverts local state equals to server state
*/
const revert = async () => {
const revert = useCallback(async () => {
setChanged(false);
await refetch();
@@ -114,26 +132,29 @@ export default function AppSettingsModal() {
setDoShowQuickEntry(showQuickEntry);
setDoStarTimeIsLastEnd(starTimeIsLastEnd);
setDoDefaultPublic(defaultPublic);
};
}, [defaultPublic, refetch, showQuickEntry, starTimeIsLastEnd]);
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
const handleChange = useCallback(
(field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
},
[formData]
);
/**
* Sets changed flag to true
*/
const handleContextChange = () => {
const handleContextChange = useCallback(() => {
setChanged(true);
};
}, []);
const disableModal = status !== 'success';
@@ -144,6 +165,7 @@ export default function AppSettingsModal() {
<br />
🔥 Changes take effect on save 🔥
</p>
<p className={style.notes}>{`Running ontime version ${version}`}</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>General App Settings</div>
@@ -197,6 +219,16 @@ export default function AppSettingsModal() {
onMouseUp={() => setHidePin(true)}
isDisabled={disableModal}
/>
<TooltipActionBtn
tooltip='Clear pincode'
size='sm'
colorScheme='red'
variant='ghost'
icon={<FiX />}
onMouseDown={() => handleChange('pinCode', '')}
onMouseUp={() => handleChange('pinCode', '')}
isDisabled={disableModal}
/>
</div>
</FormControl>
</div>
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
import { fetchEvent, postEvent } from 'app/api/eventApi';
@@ -34,36 +34,39 @@ export default function SettingsModal() {
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const submitHandler = useCallback(
async (event) => {
event.preventDefault();
setSubmitting(true);
await postEvent(formData);
await refetch();
await postEvent(formData);
await refetch();
setChanged(false);
setSubmitting(false);
};
setChanged(false);
setSubmitting(false);
},
[formData, refetch]
);
/**
* Reverts local state equals to server state
*/
const revert = async () => {
const revert = useCallback(async () => {
setChanged(false);
await refetch();
};
}, [refetch]);
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const handleChange = useCallback((field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
},[formData]);
return (
<ModalBody className={style.modalBody}>
@@ -116,9 +119,7 @@ export default function SettingsModal() {
name='pubInfo'
placeholder='Information to be shown on public screens'
value={formData.publicInfo}
onChange={(event) =>
handleChange('publicInfo', event.target.value)
}
onChange={(event) => handleChange('publicInfo', event.target.value)}
/>
</div>
<div className={style.spacedEntry}>
@@ -135,9 +136,7 @@ export default function SettingsModal() {
placeholder='Information to be shown on backstage screens'
resize={false}
value={formData.backstageInfo}
onChange={(event) =>
handleChange('backstageInfo', event.target.value)
}
onChange={(event) => handleChange('backstageInfo', event.target.value)}
/>
</div>
<div className={style.spacedEntry}>
@@ -154,9 +153,7 @@ export default function SettingsModal() {
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) =>
handleChange('endMessage', event.target.value)
}
onChange={(event) => handleChange('endMessage', event.target.value)}
/>
</div>
</div>
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
import { getInfo, httpPlaceholder, ontimeVars, postInfo } from 'app/api/ontimeApi';
@@ -36,29 +36,32 @@ export default function IntegrationSettingsModal() {
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
const submitHandler = useCallback(
async (event) => {
event.preventDefault();
const f = formData;
let e = { status: false, message: '' };
const f = formData;
const e = { status: false, message: '' };
// set fields with error
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
await postInfo(f);
setChanged(false);
setSubmitting(false);
}
};
// set fields with error
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
await postInfo(f);
setChanged(false);
setSubmitting(false);
}
},
[emitError, formData]
);
/**
* Reverts local state equals to server state
*/
const revert = async () => {
const revert = useCallback(async () => {
setChanged(false);
await refetch();
};
}, [refetch]);
// Todo: make change handler
// Todo: toggle between GET / POST
@@ -31,6 +31,7 @@
grid-template-columns: 20% 1fr 1em 1.5em 1.5em;
gap: 8px;
align-items: center;
padding: 0.5em 0;
}
.error {
+47 -41
View File
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
@@ -92,59 +92,65 @@ export default function OscSettingsModal() {
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const submitHandler = useCallback(
async (event) => {
event.preventDefault();
setSubmitting(true);
const f = formData;
const e = { status: false, message: '' };
const f = formData;
const e = { status: false, message: '' };
// Validate fields
if (f.port < 1024 || f.port > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
} else if (f.portOut < 1024 || f.portOut > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
}
// Validate fields
if (f.port < 1024 || f.port > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
} else if (f.portOut < 1024 || f.portOut > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
}
// set fields with error
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
// Post here
await postOSC(formData);
await refetch();
setChanged(false);
}
setSubmitting(false);
};
// set fields with error
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
// Post here
await postOSC(formData);
await refetch();
setChanged(false);
}
setSubmitting(false);
},
[emitError, formData, refetch]
);
/**
* Reverts local state equals to server state
*/
const revert = async () => {
const revert = useCallback(async () => {
setChanged(false);
await refetch();
};
}, [refetch]);
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {(string | number | boolean)} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
const handleChange = useCallback(
(field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
},
[formData]
);
return (
<ModalBody className={style.modalBody}>
@@ -184,7 +190,7 @@ export default function OscSettingsModal() {
name='port'
placeholder='8888'
value={formData.port}
onChange={(event) => handleChange('port', parseInt(event.target.value))}
onChange={(event) => handleChange('port', parseInt(event.target.value, 10))}
style={{ width: '6em', textAlign: 'center' }}
/>
</FormControl>
@@ -224,7 +230,7 @@ export default function OscSettingsModal() {
name='portOut'
placeholder='9999'
value={formData.portOut}
onChange={(event) => handleChange('portOut', parseInt(event.target.value))}
onChange={(event) => handleChange('portOut', parseInt(event.target.value, 10))}
style={{ width: '6em', textAlign: 'left' }}
/>
</FormControl>
@@ -11,7 +11,7 @@ export default function SubmitContainer(props) {
<Button
isDisabled={submitting || !changed}
variant='ghosted'
onClick={() => revert()}
onClick={revert}
>
Revert
</Button>
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
import { getUserFields, postUserFields, userFieldsPlaceholder } from '../../app/api/ontimeApi';
@@ -28,7 +28,7 @@ export default function TableOptionsModal() {
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
const submitHandler = useCallback(async (event) => {
event.preventDefault();
setSubmitting(true);
@@ -46,29 +46,29 @@ export default function TableOptionsModal() {
}
setSubmitting(false);
};
},[refetch, userFields]);
/**
* Reverts local state equals to server state
*/
const revert = async () => {
const revert = useCallback(async () => {
setChanged(false);
await refetch();
};
},[refetch]);
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const handleChange = useCallback((field, value) => {
if (value.length < 30) {
const temp = { ...userFields };
temp[field] = value;
setUserFields(temp);
setChanged(true);
}
};
},[userFields]);
return (
<ModalBody className={style.modalBody}>
+59 -59
View File
@@ -96,7 +96,7 @@ export default function OntimeTable({ tableData, userFields, handleUpdate, selec
saveHiddenColumns([]);
}, [saveHiddenColumns, toggleHideAllColumns]);
const handleOnDragEnd = (event) => {
const handleOnDragEnd = useCallback((event) => {
const { delta, active, over } = event;
// cancel if delta y is greater than 200
@@ -121,7 +121,7 @@ export default function OntimeTable({ tableData, userFields, handleUpdate, selec
saveColumnOrder(cols);
setColumnOrder(cols);
};
}, [columnOrder, hiddenColumns, saveColumnOrder, setColumnOrder]);
// save hidden columns object to local storage
useEffect(() => {
@@ -170,66 +170,66 @@ export default function OntimeTable({ tableData, userFields, handleUpdate, selec
)}
<table {...getTableProps()} className={style.ontimeTable}>
<thead className={style.tableHeader}>
{headerGroups.map((headerGroup) => {
const { key, ...restHeaderGroupProps } = headerGroup.getHeaderGroupProps();
return (
<DndContext
key={key}
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleOnDragEnd}
>
<tr {...restHeaderGroupProps}>
<th className={style.indexColumn}>
<Tooltip label='Event Order' openDelay={300}>
#
</Tooltip>
</th>
<SortableContext
key={key}
items={headerGroup.headers}
strategy={horizontalListSortingStrategy}
>
{headerGroup.headers.map((column) => {
const { key } = column.getHeaderProps();
return <SortableCell key={key} column={column} />;
})}
</SortableContext>
</tr>
</DndContext>
);
})}
{headerGroups.map((headerGroup) => {
const { key, ...restHeaderGroupProps } = headerGroup.getHeaderGroupProps();
return (
<DndContext
key={key}
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleOnDragEnd}
>
<tr {...restHeaderGroupProps}>
<th className={style.indexColumn}>
<Tooltip label='Event Order' openDelay={300}>
#
</Tooltip>
</th>
<SortableContext
key={key}
items={headerGroup.headers}
strategy={horizontalListSortingStrategy}
>
{headerGroup.headers.map((column) => {
const { key } = column.getHeaderProps();
return <SortableCell key={key} column={column} />;
})}
</SortableContext>
</tr>
</DndContext>
);
})}
</thead>
<tbody {...getTableBodyProps} className={style.tableBody}>
{/*This is saving in place of a default component*/}
{/* eslint-disable-next-line array-callback-return */}
{rows.map((row) => {
prepareRow(row);
const { key } = row.getRowProps();
const type = row.original.type;
if (type === 'event') {
eventIndex++;
return (
<EventRow
key={key}
row={row}
index={eventIndex}
selectedId={selectedId}
delay={cumulativeDelay}
/>
);
}
if (type === 'delay') {
if (row.original.duration != null) {
cumulativeDelay += row.original.duration;
{/*This is saving in place of a default component*/}
{/* eslint-disable-next-line array-callback-return */}
{rows.map((row) => {
prepareRow(row);
const { key } = row.getRowProps();
const type = row.original.type;
if (type === 'event') {
eventIndex++;
return (
<EventRow
key={key}
row={row}
index={eventIndex}
selectedId={selectedId}
delay={cumulativeDelay}
/>
);
}
return <DelayRow key={key} row={row} />;
}
if (type === 'block') {
cumulativeDelay = 0;
return <BlockRow key={key} row={row} />;
}
})}
if (type === 'delay') {
if (row.original.duration != null) {
cumulativeDelay += row.original.duration;
}
return <DelayRow key={key} row={row} />;
}
if (type === 'block') {
cumulativeDelay = 0;
return <BlockRow key={key} row={row} />;
}
})}
</tbody>
</table>
</>
@@ -0,0 +1,14 @@
import React from 'react';
import ProtectRoute from '../../common/components/protectRoute/ProtectRoute';
import TableWrapper from './TableWrapper';
import { TableSettingsProvider } from '../../app/context/TableSettingsContext';
export default function ProtectedTable() {
return (
<ProtectRoute>
<TableSettingsProvider>
<TableWrapper />
</TableSettingsProvider>
</ProtectRoute>
);
}
+8 -2
View File
@@ -163,14 +163,14 @@
}
.selected > td {
background-color: #4bffab99;
background-color: rgba($ontime-accent, 0.8);
}
}
.indexColumn {
font-weight: 200;
text-align: right;
width: 2em;
width: 2.5em;
background: transparent;
}
@@ -271,6 +271,12 @@ svg {
z-index: 10;
}
.check {
font-size: 1.5em;
background-color: transparent !important;
margin: 0 auto;
}
@keyframes rotation {
from {
transform: rotate(0deg);
+8 -3
View File
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useFetch } from '../../app/hooks/useFetch';
import { useSocket } from '../../app/context/socketContext';
import { TableSettingsContext } from '../../app/context/TableSettingsContext';
@@ -18,6 +18,11 @@ export default function TableWrapper() {
const [selectedId, setSelectedId] = useState(null);
const { theme } = useContext(TableSettingsContext);
// Set window title
useEffect(() => {
document.title = 'ontime - Cuesheet';
}, []);
/**
* Handle incoming data from socket
*/
@@ -38,7 +43,7 @@ export default function TableWrapper() {
};
}, [socket]);
const handleUpdate = async (rowIndex, accessor, payload) => {
const handleUpdate = useCallback(async (rowIndex, accessor, payload) => {
if (rowIndex == null || accessor == null || payload == null) {
return;
}
@@ -71,7 +76,7 @@ export default function TableWrapper() {
} catch (error) {
console.error(error);
}
};
}, [mutation, tableData]);
if (typeof tableData === 'undefined' || typeof userFields === 'undefined') {
return <span>loading...</span>;
+2 -1
View File
@@ -2,6 +2,7 @@ import React from 'react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import EditableCell from './tableElements/EditableCell';
import { stringFromMillis } from '../../common/utils/time.js';
import style from './Table.module.scss';
/**
* React - Table column object
@@ -13,7 +14,7 @@ export const makeColumns = (sizes, userFields) => {
{
Header: 'Public',
accessor: 'isPublic',
Cell: ({ cell: { value } }) => (value ? <FiCheck /> :""),
Cell: ({ cell: { value } }) => (value ? <FiCheck className={style.check} /> : ''),
width: sizes?.isPublic || 50,
},
{
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { AutoTextArea } from '../../../common/input/AutoTextArea';
import PropTypes from 'prop-types';
@@ -21,31 +21,30 @@ export default function EditableCell(props) {
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onChange = useCallback((e) => setValue(e.target.value), []);
// We'll only update the external data when the input is blurred
const onBlur = () => {
handleUpdate(index, id, value);
};
const onBlur = useCallback(() => handleUpdate(index, id, value), [handleUpdate, id, index, value]);
// If the initialValue is changed external, sync it up with our state
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<AutoTextArea
size='sm'
borderColor='#0001'
defaultValue={value}
onChange={onChange}
onBlur={onBlur}
rows={3}
transition='none'
/>
);
// If the initialValue is changed external, sync it up with our state
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<AutoTextArea
size='sm'
borderColor='#0001'
defaultValue={value}
onChange={onChange}
onBlur={onBlur}
rows={3}
transition='none'
spellCheck={false}
autoComplete={false}
/>
);
}
EditableCell.propTypes = {
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import styles from '../Table.module.scss';
export default function SortableCell({ column }) {
const { key, style, ...restColumn } = column.getHeaderProps();
const { style, ...restColumn } = column.getHeaderProps();
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: column.id,
});
@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react';
import QRCode from 'react-qr-code';
import { AnimatePresence, motion } from 'framer-motion';
import { formatDisplay } from 'common/utils/dateConfig';
import Paginator from 'common/components/views/Paginator';
import NavLogo from 'common/components/nav/NavLogo';
import { AnimatePresence, motion } from 'framer-motion';
import TitleSide from 'common/components/views/TitleSide';
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
import { titleVariants } from '../common/animation';
import PropTypes from 'prop-types';
import style from './StageManager.module.scss';
export default function StageManager(props) {
@@ -137,3 +138,12 @@ export default function StageManager(props) {
</div>
);
}
StageManager.propTypes = {
publ: PropTypes.object,
title: PropTypes.object,
time: PropTypes.object,
backstageEvents: PropTypes.object,
selectedId: PropTypes.string,
general: PropTypes.object,
};
+11 -1
View File
@@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react';
import QRCode from 'react-qr-code';
import { AnimatePresence, motion } from 'framer-motion';
import Paginator from 'common/components/views/Paginator';
import NavLogo from 'common/components/nav/NavLogo';
import { AnimatePresence, motion } from 'framer-motion';
import TitleSide from 'common/components/views/TitleSide';
import { titleVariants } from '../common/animation';
import PropTypes from 'prop-types';
import style from './Public.module.scss';
export default function Public(props) {
@@ -124,3 +125,12 @@ export default function Public(props) {
</div>
);
}
Public.propTypes = {
publ: PropTypes.object,
publicTitle: PropTypes.object,
time: PropTypes.object,
events: PropTypes.object,
publicSelectedId: PropTypes.string,
general: PropTypes.object,
};
@@ -1,30 +0,0 @@
import { AspectRatio } from '@chakra-ui/layout';
import { CircularProgress } from '@chakra-ui/progress';
import { useState } from 'react';
import style from './IFrameLoader.module.css';
export default function IFrameLoader(props) {
const [loading, setLoading] = useState(true);
const { title, src } = props;
return (
<AspectRatio maxW='300' ratio={16 / 9} className={style.iframeContainer}>
<>
{loading && (
<CircularProgress
className={style.loader}
isIndeterminate
color='orange.300'
trackColor='#FFF0'
/>
)}
<iframe
className={style.iframe}
title={title}
src={src}
onLoad={() => setLoading(false)}
/>
</>
</AspectRatio>
);
}
@@ -1,13 +0,0 @@
.iframeContainer {
margin: auto;
background-color: #0001;
border: 1px solid #0001;
}
.loader {
z-index: 100;
}
.iframe {
width: inherit;
}
+10 -2
View File
@@ -1,11 +1,12 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import QRCode from 'react-qr-code';
import style from './Pip.module.scss';
import { AnimatePresence, motion } from 'framer-motion';
import Paginator from 'common/components/views/Paginator';
import NavLogo from 'common/components/nav/NavLogo';
import { AnimatePresence, motion } from 'framer-motion';
import { formatDisplay } from 'common/utils/dateConfig';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
import PropTypes from 'prop-types';
import style from './Pip.module.scss';
export default function Pip(props) {
const { time, backstageEvents, selectedId, general } = props;
@@ -121,3 +122,10 @@ export default function Pip(props) {
</div>
);
}
Pip.propTypes = {
time: PropTypes.object,
backstageEvents: PropTypes.object,
selectedId: PropTypes.string,
general: PropTypes.object,
};
@@ -20,7 +20,7 @@ export default function LowerClean(props) {
// Calculate time
const fadeOutTime =
(parseInt(options.fadeOut) +
(parseInt(options.fadeOut, 10) +
(options.transitionIn || defaults.transitionIn)) *
1000;
if (isNaN(fadeOutTime)) return;
@@ -32,9 +32,6 @@ export default function LowerClean(props) {
return () => clearTimeout(timeout);
}, [options.fadeOut, options.transitionIn, defaults.transitionIn]);
// calculate transition times
useEffect(() => {});
// Format messages
const showLowerMessage = lower.text !== '' && lower.visible;
@@ -19,7 +19,7 @@ export default function LowerLines(props) {
// Calculate time
const fadeOutTime =
(parseInt(options.fadeOut) +
(parseInt(options.fadeOut, 10) +
(options.transitionIn || defaults.transitionIn)) *
1000;
if (isNaN(fadeOutTime)) return;
@@ -69,7 +69,7 @@ const Lower = (props) => {
// preset: selector
// Should be a number 1-n
const p = parseInt(searchParams.get('preset'));
const p = parseInt(searchParams.get('preset'), 10);
if (!isNaN(p)) setPreset(p);
// size: multiplier
@@ -79,7 +79,7 @@ const Lower = (props) => {
// transitionIn: seconds
// Should be a number 0-n
const t = parseInt(searchParams.get('transition'));
const t = parseInt(searchParams.get('transition'), 10);
if (!isNaN(t)) options.transitionIn = t;
// textColour: string
@@ -99,17 +99,17 @@ const Lower = (props) => {
// fadeOut: seconds
// Should be a number 0-n
const f = parseInt(searchParams.get('fadeout'));
const f = parseInt(searchParams.get('fadeout'), 10);
if (!isNaN(f)) options.fadeOut = f;
// x: pixels
// Should be a number 0-n
const x = parseInt(searchParams.get('x'));
const x = parseInt(searchParams.get('x'), 10);
if (!isNaN(x)) options.posX = x;
// y: pixels
// Should be a number 0-n
const y = parseInt(searchParams.get('y'));
const y = parseInt(searchParams.get('y'), 10);
if (!isNaN(y)) options.posY = y;
setLowerOptions({
@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { formatDisplay } from '../../../common/utils/dateConfig';
import NavLogo from '../../../common/components/nav/NavLogo';
import PropTypes from 'prop-types';
import style from './MinimalTimer.module.scss';
export default function MinimalTimer(props) {
@@ -31,3 +32,8 @@ export default function MinimalTimer(props) {
</div>
);
}
MinimalTimer.propTypes = {
pres: PropTypes.object,
time: PropTypes.object,
};
@@ -5,6 +5,7 @@ import Countdown from 'common/components/countdown/Countdown';
import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
import NavLogo from 'common/components/nav/NavLogo';
import TitleCard from 'common/components/views/TitleCard';
import PropTypes from 'prop-types';
import style from './Timer.module.scss';
export default function Timer(props) {
@@ -149,3 +150,10 @@ export default function Timer(props) {
</div>
);
}
Timer.propTypes = {
general: PropTypes.object,
pres: PropTypes.object,
title: PropTypes.object,
time: PropTypes.object,
};