Feat/55 v2 (#225)

* chore: upgrade relevant libraries
* feat(skip): add skip styling to paginated items
* chore: upgrade relevant libraries
* Fix: issue with text colour (#214)
* fix: adjust text colour from context
* fix: issue with wrong proptype
* fix issue with vite migration (#217)
* hotfix: 1.8.2 issues vite migration
* fix: file import options
* feat(55): styling
* refactor: remove onhover option for entry block
* refactor: relocate logging provider
* refactor: convert to typescript
* feat(55): add event editor
* refactor: extract event actions
* fix: bad import
* feat(55): redesign components
* fix: issues with not awaiting async
* refactor: replace socket with subscription
* refactor: remove unused
* style: small tweaks
* refactor: extract event actions
* refactor: cleanup debug
* refactor: chakra imports
* fix: optimistic mutations
* refactor: handle promise rejections
* refactor: validation
* fix: rq optimistic mutations
* feat: add playback feedback to block
* refactor: no optimistic adding of events
* refactor: simplify cursor state
* styles: cleanup editor style
* refactor: cleanup duration update
* fix: revert package upgrade (issues with vitest)
* fix: prevent cyclic imports
* refactor: extract data fetcher
* refactor: typescript migration
* chore: update tests
This commit is contained in:
Carlos Valente
2022-10-19 21:30:25 +02:00
committed by GitHub
parent 13be6ea2bc
commit 0fea4064c3
157 changed files with 4072 additions and 3033 deletions
@@ -1,7 +1,5 @@
import { useEffect, useState } from 'react';
import { IconButton } from '@chakra-ui/button';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { Tooltip } from '@chakra-ui/tooltip';
import { Editable, EditableInput, EditablePreview, IconButton, Tooltip } from '@chakra-ui/react';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { tooltipDelayMid } from '../../../ontimeConfig';
@@ -1,5 +1,4 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { IconButton, Tooltip } from '@chakra-ui/react';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
@@ -1,4 +1,4 @@
import { Box } from '@chakra-ui/layout';
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
@@ -1,6 +1,5 @@
import { memo } from 'react';
import { Button } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/react';
import { Button, Tooltip } from '@chakra-ui/react';
import TimerDisplay from 'common/components/countdown/TimerDisplay';
import PropTypes from 'prop-types';
@@ -55,7 +54,6 @@ const PlaybackTimer = (props) => {
{isWaiting ? (
<div className={style.roll}>
<span className={style.rolltag}>Roll: Countdown to start</span>
<span className={style.time}>FIX</span>
</div>
) : (
<>
@@ -1,4 +1,4 @@
import { Box } from '@chakra-ui/layout';
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
@@ -1,57 +0,0 @@
@use '../../../theme/main' as *;
/* ============= COMMON ============= */
.block {
margin: 0.2em 0;
padding: 0.2em 0.5em;
box-sizing: border-box;
width: 100%;
border-radius: 8px;
font-size: 15px;
border: $block-border;
display: grid;
grid-template-columns: 2em auto;
align-content: center;
align-items: baseline;
gap: 0.5em;
background-color: $block-block-color;
background: linear-gradient(
0deg,
rgba(107, 70, 193, 0.4) 0%,
rgba(128, 90, 213, 0.4) 20%,
rgba(107, 70, 193, 0.15) 21%
);
}
/* ================ DRAG ================ */
.drag {
color: rgba(255, 255, 255, 0.67);
align-self: center;
justify-self: start;
}
/* ============== ACTION ================ */
.actionOverlay {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
align-self: flex-start;
padding-top: 0.2em;
opacity: 0.8;
transition: linear 0.1s;
justify-self: end;
}
.block:hover > .actionOverlay {
opacity: 1;
transition: linear 0.1s;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
}
@@ -1,67 +0,0 @@
@use '../../../theme/main' as *;
/* ============= COMMON ============= */
.delay {
margin: 0.2em 0;
padding: 0.2em 0.5em;
width: 100%;
border-radius: 8px;
font-size: 15px;
border: $block-border;
display: grid;
grid-template-columns: 2em 1fr auto;
grid-template-areas: 'drag inpt btns';
justify-content: center;
align-content: center;
align-items: baseline;
gap: 0.5em;
background-color: $block-delay-color;
background: linear-gradient(
180deg,
rgba(214, 158, 46, 0.4) 0%,
rgba(236, 201, 75, 0.4) 20%,
rgba(214, 158, 46, 0.07) 21%
);
}
/* ================ DRAG ================ */
.drag {
grid-area: drag;
color: $block-icon-drag;
align-self: center;
}
/* =============== INPUT ================ */
.input {
grid-area: inpt;
}
/* ============== ACTION ================ */
.actionOverlay {
grid-area: btns;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
align-self: flex-start;
padding-top: 0.2em;
opacity: 0.8;
transition: linear 0.1s;
}
.delay:hover > .actionOverlay {
opacity: 1;
transition: linear 0.1s;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
}
+12 -1
View File
@@ -148,10 +148,21 @@ h1 {
left: 0;
z-index: 10;
color: white;
transition: bottom 0.3s;
&.noEvent {
bottom: -500px;
transition: bottom 0.7s;
}
.header {
display: flex;
justify-content: space-between;
}
.header {
h1 {
margin-right: auto;
}
}
}
+6 -5
View File
@@ -1,11 +1,10 @@
import { lazy, useEffect } from 'react';
import { useDisclosure } from '@chakra-ui/hooks';
import { Box } from '@chakra-ui/layout';
import { Box } from '@chakra-ui/react';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import UploadModal from 'common/components/upload-modal/UploadModal';
import ModalManager from 'features/modals/ModalManager';
import UploadModal from '../../common/components/upload-modal/UploadModal';
import { LoggingProvider } from '../../common/context/LoggingContext';
import MenuBar from '../menu/MenuBar';
import styles from './Editor.module.scss';
@@ -14,6 +13,7 @@ const EventList = lazy(() => import('features/editors/list/EventListExport'));
const TimerControl = lazy(() => import('features/control/playback/TimerControlExport'));
const MessageControl = lazy(() => import('features/control/message/MessageControlExport'));
const Info = lazy(() => import('features/info/InfoExport'));
const EventEditor = lazy(() => import('features/event-editor/EventEditorExport'));
export default function Editor() {
const {
@@ -34,7 +34,7 @@ export default function Editor() {
}, []);
return (
<LoggingProvider>
<>
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
<ErrorBoundary>
<ModalManager isOpen={isSettingsOpen} onClose={onSettingsClose} />
@@ -56,6 +56,7 @@ export default function Editor() {
<TimerControl />
<Info />
</div>
</LoggingProvider>
<EventEditor />
</>
);
}
@@ -1,71 +0,0 @@
import { memo } from 'react';
import { HStack } from '@chakra-ui/react';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import PropTypes from 'prop-types';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
import EventTimes from '../../../common/components/eventTimes/EventTimes';
import EditableText from '../../../common/components/input/EditableText';
import style from './EventBlock.module.scss';
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 (
<>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
</span>
<div className={style.indicators}>
<span className={next ? style.next : style.nextDisabled}>Next</span>
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
</div>
<EventTimes
actionHandler={actionHandler}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
delay={delay}
previousEnd={previousEnd}
className={style.time}
/>
<div className={style.titleContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
/>
</div>
<HStack spacing='0.5em' className={style.actionOverlay}>
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
</HStack>
</>
);
}
CollapsedBlock.propTypes = {
provided: PropTypes.any.isRequired,
data: PropTypes.object.isRequired,
next: PropTypes.bool.isRequired,
delay: PropTypes.any,
delayValue: PropTypes.string,
previousEnd: PropTypes.number.isRequired,
actionHandler: PropTypes.func.isRequired,
};
export default memo(CollapsedBlock, areEqual);
@@ -1,79 +0,0 @@
import { useContext, useMemo } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import Icon from '@chakra-ui/icon';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import { millisToMinutes } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import { CollapseContext } from '../../../common/context/CollapseContext';
import CollapsedBlock from './CollapsedBlock';
import ExpandedBlock from './ExpandedBlock';
import style from './EventBlock.module.scss';
export default function EventBlock(props) {
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler, next } = props;
const { isCollapsed, setCollapsed } = useContext(CollapseContext);
const collapsed = useMemo(() => isCollapsed(data.id), [data.id, isCollapsed]);
const selectedStyle = selected ? style.active : '';
const collapsedStyle = collapsed ? style.collapsed : style.expanded;
const classSelect = `${style.event} ${collapsedStyle} ${selectedStyle}`;
// Calculate delay in min
let delayValue = null;
if (delay != null && delay !== 0) {
delayValue = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
}
const handleCollapse = (isCollapsed) => {
setCollapsed(data.id, isCollapsed);
};
return (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
<div className={classSelect} {...provided.draggableProps} ref={provided.innerRef}>
<Icon
className={collapsed ? style.moreCollapsed : style.moreExpanded}
as={FiChevronUp}
onClick={() => handleCollapse(!collapsed)}
/>
{collapsed ? (
<CollapsedBlock
provided={provided}
data={data}
next={next}
delay={delay}
delayValue={delayValue}
previousEnd={previousEnd}
actionHandler={actionHandler}
/>
) : (
<ExpandedBlock
provided={provided}
eventIndex={eventIndex}
data={data}
next={next}
delay={delay}
delayValue={delayValue}
previousEnd={previousEnd}
actionHandler={actionHandler}
/>
)}
</div>
)}
</Draggable>
);
}
EventBlock.propTypes = {
data: PropTypes.object.isRequired,
selected: PropTypes.bool.isRequired,
delay: PropTypes.number,
index: PropTypes.number.isRequired,
eventIndex: PropTypes.number.isRequired,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
next: PropTypes.bool,
};
@@ -1,221 +0,0 @@
@use '../../../theme/main' as *;
/* ============= COMMON ============= */
.event {
margin: 0.2em 0;
padding: 0.2em 0.5em;
box-sizing: border-box;
width: 100%;
border-radius: 8px;
font-size: 15px;
border: 1px solid rgba(255, 255, 255, 0.05);
background-color: $bg-gray-950;
display: grid;
gap: 0.5em;
}
.active {
background-color: rgba(255, 255, 255, 0.36);
background: linear-gradient(
180deg,
#00000000 10%,
#4bffab10 97%,
#227c52aa 98%,
#4bffabcc 100%
);
}
.collapsed {
grid-template-columns: 2em 2em auto auto 1fr auto 3.7em;
grid-template-areas: 'drag indi time time text more btns';
justify-content: center;
align-items: center;
}
.expanded {
grid-template-columns: 2em 2em 2em auto 1fr auto 3.7em;
grid-template-rows: repeat(4, 1fr);
grid-template-areas:
'drag indi time time text more btns'
'.... indi time time text .... btns'
'.... indi time time text .... btns'
'.... indi time time text .... btns';
justify-content: center;
}
/* ================ DRAG ================ */
.drag {
grid-area: drag;
color: rgba(255, 255, 255, 0.67);
align-self: center;
}
/* ============= INDICATORS ============= */
.indicators {
grid-area: indi;
display: flex;
flex-direction: column;
align-self: flex-start;
font-size: 0.75em;
overflow: hidden;
min-height: 3em;
min-width: 0;
justify-content: center;
}
.next,
.nextDisabled {
width: max-content;
padding: 0 0.2em;
color: $ontime-accent;
transition: 0.3s;
}
.next {
opacity: 0.8;
}
.nextDisabled {
opacity: 0.05;
}
.delayValue {
color: #d69e2eaa;
padding: 0 0.2em;
text-align: center;
align-self: flex-start;
width: max-content;
}
.next:after,
.delayValue:after {
content: '\200b';
}
/* =============== TIMES ================ */
.time {
grid-area: time;
align-self: center;
}
.start {
grid-area: star;
}
.end {
grid-area: end;
}
.duration {
grid-area: dura;
}
.timeExpanded {
grid-area: time;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-items: flex-start;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 4px;
padding: 0 0.5em 0.5em 0.5em;
}
.label {
font-size: 0.75em;
color: #aaa;
}
.calculatedTime {
grid-area: time;
font-size: 0.75em;
}
/* =============== TITLES =============== */
.titleContainer {
grid-area: text;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
width: 100%;
display: block;
white-space: nowrap;
overflow: hidden;
}
.collapsed .titleContainer {
height: fit-content;
}
.expanded .titleContainer {
height: 100%;
border-radius: 4px;
}
.oscLabel {
color: #4bffabcc;
font-size: 0.8em;
float: right;
padding-right: 1em;
-webkit-user-select: text;
user-select: text;
}
/* ================ MORE ================ */
.moreExpanded,
.moreCollapsed {
cursor: pointer;
color: #fff;
grid-area: more;
margin-top: 0.2em;
align-self: baseline;
}
.moreExpanded {
transform: scaleY(-1);
transition: transform 0.3s;
}
.moreCollapsed {
transform: scaleY(1);
transition: transform 0.3s;
}
/* ============== ACTION ================ */
.actionOverlay {
grid-area: btns;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
align-self: flex-start;
padding-top: 0.2em;
opacity: 0.8;
transition: linear 0.1s;
}
.expanded > .actionOverlay {
flex-direction: column;
justify-self: flex-end;
}
.event:hover > .actionOverlay {
opacity: 1;
transition: linear 0.1s;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
}
@@ -1,111 +0,0 @@
import { memo } from 'react';
import { VStack } from '@chakra-ui/react';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import PropTypes from 'prop-types';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
import EventTimesVertical from '../../../common/components/eventTimes/EventTimesVertical';
import EditableText from '../../../common/components/input/EditableText';
import style from './EventBlock.module.scss';
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 || '...';
return (
<>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
</span>
<div className={style.indicators}>
<span className={next ? style.next : style.nextDisabled}>Next</span>
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
</div>
<div className={style.timeExpanded}>
<EventTimesVertical
actionHandler={actionHandler}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
duration={data.duration}
delay={delay}
previousEnd={previousEnd}
className={style.time}
/>
</div>
<div className={style.titleContainer}>
<EditableText
label='Title'
defaultValue={data.title}
placeholder='Add Title'
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
/>
<EditableText
label='Presenter'
defaultValue={data.presenter}
placeholder='Add Presenter name'
submitHandler={(v) => actionHandler('update', { field: 'presenter', value: v })}
/>
<EditableText
label='Subtitle'
defaultValue={data.subtitle}
placeholder='Add Subtitle'
submitHandler={(v) => actionHandler('update', { field: 'subtitle', value: v })}
/>
<EditableText
label='Note'
defaultValue={data.note}
placeholder='Add Note'
style={{ color: '#d69e2e' }}
maxchar={160}
submitHandler={(v) => actionHandler('update', { field: 'note', value: v })}
/>
<span className={style.oscLabel}>
{`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`}
</span>
</div>
<VStack spacing='0.5em' className={style.actionOverlay}>
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
icon={<IoRemove />}
colorScheme='red'
tooltip='Delete'
_hover={{ bg: 'red.400' }}
/>
</VStack>
</>
);
}
ExpandedBlock.propTypes = {
provided: PropTypes.any.isRequired,
data: PropTypes.object.isRequired,
eventIndex: PropTypes.number.isRequired,
next: PropTypes.bool.isRequired,
delay: PropTypes.number,
delayValue: PropTypes.string,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
};
export default memo(ExpandedBlock, areEqual);
@@ -0,0 +1,36 @@
$block-gap: 4px;
$element-spacing: 4px;
$binder-width: 32px;
$clearance: 8px;
$block-border-radius: 3px;
@mixin block-spacing() {
margin: 4px 1px;
padding: 4px 10px 4px 2px;
gap: 2px;
}
@mixin action-overlay() {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
opacity: 0.8;
transition: linear 0.1s;
}
@mixin block-hover() {
opacity: 1;
transition: linear 0.1s;
}
@mixin drag-style() {
font-size: 20px;
text-align: center;
opacity: 0.1;
cursor: grab;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
@@ -1,7 +1,7 @@
import { Draggable } from 'react-beautiful-dnd';
import { HStack } from '@chakra-ui/react';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import PropTypes from 'prop-types';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
@@ -15,15 +15,11 @@ export default function BlockBlock(props) {
return (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
<div
className={style.block}
{...provided.draggableProps}
ref={provided.innerRef}
>
<div className={style.block} {...provided.draggableProps} ref={provided.innerRef}>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
<IoReorderTwo />
</span>
<HStack spacing='0.5em' className={style.actionOverlay}>
<HStack spacing='4px' className={style.actionOverlay}>
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
icon={<IoRemove />}
@@ -39,7 +35,6 @@ export default function BlockBlock(props) {
);
}
BlockBlock.propTypes = {
index: PropTypes.number.isRequired,
data: PropTypes.object.isRequired,
@@ -0,0 +1,42 @@
@use '../../../theme/main' as *;
@use '../blockMixins' as *;
/* ============= COMMON ============= */
.block {
@include block-spacing;
display: grid;
grid-template-columns: 40px 1fr;
align-items: center;
height: 40px;
border-radius: 2px 2px $block-border-radius $block-border-radius;
background-color: rgba(107, 70, 193, 0.4);
background: linear-gradient(
0deg,
rgba(107, 70, 193, 0.4) 0%,
rgba(128, 90, 213, 0.4) 20%,
rgba(107, 70, 193, 0.15) 21%
);
}
/* ================ DRAG ================ */
.drag {
@include drag-style;
}
/* ============== ACTION ================ */
.actionOverlay {
@include action-overlay;
justify-self: end;
}
.block:hover > .actionOverlay {
@include block-hover;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
}
@@ -1,45 +1,68 @@
import { useCallback } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { HStack } from '@chakra-ui/react';
import { Button, HStack } from '@chakra-ui/react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import ActionButtons from 'common/components/buttons/ActionButtons';
import TooltipLoadingActionBtn from 'common/components/buttons/TooltipLoadingActionBtn';
import DelayInput from 'common/components/input/DelayInput';
import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
import style from './DelayBlock.module.scss';
export default function DelayBlock(props) {
const { eventsHandler, data, index, actionHandler } = props;
const { data, index, actionHandler } = props;
const { applyDelay, deleteEvent, updateEvent } = useEventAction();
const applyDelayHandler = useCallback(() => {
eventsHandler('applyDelay', { id: data.id, duration: data.duration });
}, [data.duration, data.id, eventsHandler]);
applyDelay(data.id);
}, [data.id, applyDelay]);
const deleteHandler = useCallback(() => {
deleteEvent(data.id);
}, [data.id, deleteEvent]);
const delaySubmitHandler = useCallback(
(value) => {
const newEvent = {
id: data.id,
duration: value * 60000,
};
updateEvent(newEvent);
},
[data.id, updateEvent]
);
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
return (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
<div className={style.delay} {...provided.draggableProps} ref={provided.innerRef}>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
<IoReorderTwo />
</span>
<DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} />
<HStack spacing='0.5em' className={style.actionOverlay}>
<TooltipActionBtn
clickHandler={applyDelayHandler}
icon={<FiCheck />}
<DelayInput
className={style.input}
value={delayValue}
submitHandler={delaySubmitHandler}
/>
<HStack spacing='4px' className={style.actionOverlay}>
<Button
onClick={applyDelayHandler}
size='xs'
colorScheme='orange'
tooltip='Apply delays'
_hover={{ bg: 'orange.400' }}
/>
leftIcon={<FiCheck />}
>
Apply delay
</Button>
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
clickHandler={deleteHandler}
icon={<IoRemove />}
colorScheme='red'
tooltip='Delete'
@@ -54,7 +77,6 @@ export default function DelayBlock(props) {
}
DelayBlock.propTypes = {
eventsHandler: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
actionHandler: PropTypes.func.isRequired,
@@ -0,0 +1,51 @@
@use '../../../theme/main' as *;
@use '../blockMixins' as *;
/* ============= COMMON ============= */
.delay {
@include block-spacing;
display: grid;
grid-template-columns: 32px 1fr auto;
grid-template-areas: 'drag inpt btns';
align-items: center;
height: 40px;
border-radius: $block-border-radius $block-border-radius 2px 2px;
background-color: rgba(214, 158, 46, 0.4);
background: linear-gradient(
180deg,
rgba(214, 158, 46, 0.4) 0%,
rgba(214, 158, 46, 0.4) 20%,
rgba(214, 158, 46, 0.17) 21%
);
}
/* ================ DRAG ================ */
.drag {
grid-area: drag;
@include drag-style;
}
/* =============== INPUT ================ */
.input {
grid-area: inpt;
}
/* ============== ACTION ================ */
.actionOverlay {
@include action-overlay;
}
.delay:hover > .actionOverlay {
@include block-hover;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
}
@@ -1,31 +1,62 @@
import { useEffect, useState } from 'react';
import { Checkbox } from '@chakra-ui/react';
import { Tooltip } from '@chakra-ui/tooltip';
import { useCallback, useContext, useEffect, useState } from 'react';
import { Checkbox, Tooltip } from '@chakra-ui/react';
import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction';
import { EventTypes } from 'common/models/EventTypes';
import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types';
import {
defaultPublicAtom,
startTimeIsLastEndAtom,
} from '../../../common/atoms/LocalEventSettings';
import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './EntryBlock.module.scss';
export default function EntryBlock(props) {
interface EntryBlockProps {
showKbd: boolean;
previousId?: string;
visible?: boolean;
disableAddDelay?: boolean;
disableAddBlock: boolean;
}
export default function EntryBlock(props: EntryBlockProps) {
const {
showKbd,
previousId,
eventsHandler,
visible,
visible = true,
disableAddDelay = true,
disableAddBlock,
} = props;
const { addEvent } = useEventAction();
const { emitError } = useContext(LoggingContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const [doStartTime, setStartTime] = useState(startTimeIsLastEnd);
const [doPublic, setPublic] = useState(defaultPublic);
const handleCreateEvent = useCallback((eventType: EventTypes) => {
switch (eventType) {
case 'event': {
const newEvent = { type: 'event', after: previousId, isPublic: doPublic };
const options = { startIsLastEnd: doStartTime ? previousId : undefined };
addEvent(newEvent, options);
break;
}
case 'delay': {
addEvent({ type: 'delay', after: previousId });
break;
}
case 'block': {
addEvent({ type: 'block', after: previousId });
break;
}
default: {
emitError(`Cannot create unknown event type: ${eventType}`);
break;
}
}
}, [addEvent, doPublic, doStartTime, emitError, previousId]);
useEffect(() => {
setStartTime(startTimeIsLastEnd);
}, [startTimeIsLastEnd]);
@@ -39,13 +70,7 @@ export default function EntryBlock(props) {
<Tooltip label='Add Event' openDelay={tooltipDelayMid}>
<span
className={style.createEvent}
onClick={() =>
eventsHandler(
'add',
{ type: 'event', after: previousId, isPublic: doPublic },
{ startIsLastEnd: doStartTime ? previousId : undefined }
)
}
onClick={() => handleCreateEvent('event')}
role='button'
>
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
@@ -54,7 +79,7 @@ export default function EntryBlock(props) {
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
<span
className={`${style.createDelay} ${disableAddDelay ? style.disabled : ''}`}
onClick={() => eventsHandler('add', { type: 'delay', after: previousId })}
onClick={() => handleCreateEvent('delay')}
role='button'
>
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
@@ -63,7 +88,7 @@ export default function EntryBlock(props) {
<Tooltip label='Add Block' openDelay={tooltipDelayMid}>
<span
className={`${style.createBlock} ${disableAddBlock ? style.disabled : ''}`}
onClick={() => eventsHandler('add', { type: 'block', after: previousId })}
onClick={() => handleCreateEvent('block')}
role='button'
>
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
@@ -74,9 +99,7 @@ export default function EntryBlock(props) {
size='sm'
colorScheme='blue'
isChecked={doStartTime}
onChange={(e) => {
setStartTime(e.target.checked);
}}
onChange={(e) => setStartTime(e.target.checked)}
>
Start time is last end
</Checkbox>
@@ -92,12 +115,3 @@ export default function EntryBlock(props) {
</div>
);
}
EntryBlock.propTypes = {
showKbd: PropTypes.bool,
eventsHandler: PropTypes.func,
visible: PropTypes.bool,
previousId: PropTypes.string,
disableAddDelay: PropTypes.bool,
disableAddBlock: PropTypes.bool,
};
@@ -0,0 +1,220 @@
@use '../../../theme/main' as *;
@use '../blockMixins' as *;
.eventBlock {
// layout
display: grid;
grid-template-areas:
"binder progb progb progb"
"binder pb-actions times actions"
"binder pb-actions title title"
"binder pb-actions notes status"
"binder ... ... ...";
grid-template-columns: $binder-width auto 1fr auto;
grid-template-rows: $element-spacing 36px 36px 36px $element-spacing;
align-items: center;
margin: 2px 1px;
padding-right: $clearance;
// style - general
border-radius: $block-border-radius;
gap: 2px;
// style - colour
background-color: $bg-gray-950;
border: $block-border;
// style - text
// variant
&.selected {
background-color: $bg-gray-900;
border: 1px solid $ontime-accent;
}
&.skip {
background-color: $bg-gray-1000;
border: 1px solid transparent;
.eventTimers > .delayNote,
.eventTitle,
.eventNote,
{
color: $text-gray-disabled;
}
.binder,
.eventTimers,
.eventStatus {
opacity: $opacity-disabled;
}
}
.binder {
// layout
grid-area: binder;
height: 100%;
display: grid;
place-content: center;
position: relative;
cursor: pointer;
// style - general
border-radius: 3px 0 0 3px;
border-right: 1px solid $bg-gray-900;
// style - colour
background-color: $bg-gray-950; // override inline
color: $text-white;
// style - text
.drag {
@include drag-style;
position: absolute;
}
}
.playbackActions {
// layout
grid-area: pb-actions;
display: flex;
flex-direction: column;
margin: $element-spacing $clearance $element-spacing $element-spacing;
// style - general
gap: 6px;
}
.eventTimers {
grid-area: times;
display: flex;
align-items: center;
gap: $clearance;
height: 100%;
.delayNote {
font-size: 10px;
color: $text-delay;
}
}
.eventTitle {
grid-area: title;
display: block;
font-size: 18px;
.eventTitle__preview {
width: 100%;
}
&.noTitle {
span {
opacity: $opacity-disabled;
}
}
input {
border-radius: 2px;
border: 1px;
}
}
.eventNote {
grid-area: notes;
display: block;
font-size: 14px;
color: $notes-color;
line-height: 15px;
}
.eventActions {
grid-area: actions;
display: flex;
gap: $element-spacing;
justify-content: flex-end;
}
.eventStatus {
grid-area: status;
display: flex;
justify-content: flex-end;
gap: $element-spacing;
.statusIcon {
width: 24px;
height: 24px;
border-radius: 12px;
padding: 4px;
color: $text-gray-disabled;
}
.statusIcon.statusNext {
border: 1px solid transparent;
&.enabled {
color: $ontime-accent;
border: 1px solid $ontime-accent;
background-color: $bg-gray-1100;
}
}
.statusIcon.statusDelay {
&.enabled {
color: $text-white;
background-color: $ontime-delay;
}
}
.statusIcon.statusPublic {
&.enabled {
color: $text-white;
background-color: $ontime-roll;
}
}
}
.eventOptions {
margin: $element-spacing 16px $element-spacing 0;
}
}
.progressBg {
grid-area: progb;
border-radius: 1px;
background-color: $bg-black-100;
// layout
height: 100%;
.progressBar {
// layout
height: 100%;
width: 0;
border-radius: 1px 0 0 1px;
margin-top: 2px;
margin-left: 4px;
// animations
transition: 1s linear;
transition-property: width;
&.start {
background-color: $ontime-accent;
}
&.pause {
background-color: $ontime-paused;
}
&.roll {
background-color: $ontime-roll;
}
&.overtime {
background-color: red;
}
}
}
@@ -0,0 +1,243 @@
import { useCallback, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { Editable, EditableInput, EditablePreview, IconButton, Tooltip } from '@chakra-ui/react';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircleSharp } from '@react-icons/all-files/io5/IoRemoveCircleSharp';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { IoReturnDownForward } from '@react-icons/all-files/io5/IoReturnDownForward';
import { IoSettingsSharp } from '@react-icons/all-files/io5/IoSettingsSharp';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { editorEventId } from 'common/atoms/LocalEventSettings';
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
import { getAccessibleColour } from 'common/utils/styleUtils';
import { useAtom } from 'jotai';
import { useEventProvider } from '../../../common/hooks/useSocketProvider';
import { Playstate } from '../../../common/models/OntimeTypes';
import { tooltipDelayMid } from '../../../ontimeConfig';
import { EventItemActions } from '../list/EventListItem';
import EventBlockActionMenu from './composite/EventBlockActionMenu';
import EventBlockTimers from './composite/EventBlockTimers';
import style from './EventBlock.module.scss';
const blockBtnStyle = {
size: 'sm',
colorScheme: 'blue',
variant: 'outline',
borderRadius: '3px',
fontSize: '20px',
};
const tooltipProps = {
openDelay: tooltipDelayMid,
};
interface EventBlockProps {
timeStart: number;
timeEnd: number;
duration: number;
index: number;
eventIndex: number;
eventId: string;
isPublic: boolean;
title: string;
note: string;
delay: number;
previousEnd: number;
colour: string;
next: boolean;
skip: boolean;
selected: boolean;
playback?: Playstate;
actionHandler: (action: EventItemActions, payload: any) => void;
}
export default function EventBlock(props: EventBlockProps) {
const {
timeStart,
timeEnd,
duration,
index,
eventIndex,
eventId,
isPublic = true,
title,
note,
delay,
previousEnd,
colour,
next,
skip = false,
selected,
playback,
actionHandler,
} = props;
const [openId, setOpenId] = useAtom(editorEventId);
const { setPlayback } = useEventProvider(eventId);
const [blockTitle, setBlockTitle] = useState<string>(title || '');
const binderColours = colour && getAccessibleColour(colour);
const hasDelay = delay !== 0 && delay !== null;
const handleTitle = useCallback(
(text: string) => {
if (text === title) {
return;
}
const cleanVal = text.trim();
setBlockTitle(cleanVal);
// Todo: no need for action handler
actionHandler('update', { field: 'title', value: cleanVal });
},
[actionHandler, title],
);
// Todo: data should come from socket
const progress = `${Math.random() * 100}%`;
const eventIsPlaying = selected && playback === 'start';
return (
<Draggable key={eventId} draggableId={eventId} index={index}>
{(provided) => (
<div
className={`${style.eventBlock} ${skip ? style.skip : ''} ${
selected ? style.selected : ''
}`}
{...provided.draggableProps}
ref={provided.innerRef}
>
<div
className={style.binder}
style={{ ...binderColours }}
tabIndex={-1}
onClick={() => actionHandler('set-cursor', index)}
>
<span className={style.drag} {...provided.dragHandleProps}>
<IoReorderTwo />
</span>
{eventIndex}
</div>
<div className={selected ? style.progressBg : ''}>
<div
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
style={{ width: progress }}
/>
</div>
<div className={style.playbackActions}>
<TooltipActionBtn
aria-label='Skip event'
tooltip='Skip event'
openDelay={tooltipDelayMid}
icon={<IoRemoveCircleSharp />}
{...blockBtnStyle}
variant={skip ? 'solid' : 'outline'}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
tabIndex={-1}
disabled={selected}
/>
<TooltipActionBtn
aria-label='Start event'
tooltip='Start event'
openDelay={tooltipDelayMid}
icon={eventIsPlaying ? <IoPause /> : <IoPlay />}
disabled={skip}
{...blockBtnStyle}
variant={eventIsPlaying ? 'solid' : 'outline'}
clickHandler={
eventIsPlaying
? () => setPlayback.pause()
: () => setPlayback.startEvent()
}
tabIndex={-1}
/>
<TooltipActionBtn
aria-label='Load event'
tooltip='Load event'
openDelay={tooltipDelayMid}
icon={selected ? <IoReload /> : <IoReload />}
disabled={skip}
{...blockBtnStyle}
variant={selected ? 'solid' : 'outline'}
clickHandler={() => setPlayback.loadEvent()}
tabIndex={-1}
/>
</div>
<EventBlockTimers
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
delay={delay}
actionHandler={actionHandler}
previousEnd={previousEnd}
/>
<Editable
value={blockTitle}
className={`${style.eventTitle} ${!title || title === '' ? style.noTitle : ''}`}
placeholder='Event title'
onChange={(value) => setBlockTitle(value)}
onSubmit={(value) => handleTitle(value)}
>
<EditablePreview className={style.eventTitle__preview} />
<EditableInput />
</Editable>
<span className={style.eventNote}>{note}</span>
<div className={style.eventActions}>
<IconButton
icon={<IoSettingsSharp />}
aria-label='event options'
onClick={() => setOpenId((prev) => prev === eventId ? null : eventId)}
{...blockBtnStyle}
variant={openId === eventId ? 'solid' : 'outline'}
tabIndex={-1}
/>
<EventBlockActionMenu
showAdd
showDelay
showBlock
showClone
actionHandler={actionHandler}
/>
</div>
<div className={style.eventStatus}>
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}>
<span
className={`${style.statusIcon} ${style.statusNext} ${next ? style.enabled : ''}`}
>
<IoReturnDownForward />
</span>
</Tooltip>
<Tooltip label='Event has delay' isDisabled={!hasDelay} {...tooltipProps}>
<span
className={`${style.statusIcon} ${style.statusDelay} ${
hasDelay ? style.enabled : ''
}`}
>
<IoTimerOutline />
</span>
</Tooltip>
<Tooltip
label={`${isPublic ? 'Event is public' : 'Event is private'}`}
{...tooltipProps}
>
<span
className={`${style.statusIcon} ${style.statusPublic} ${
isPublic ? style.enabled : ''
}`}
>
<FiUsers />
</span>
</Tooltip>
</div>
</div>
)}
</Draggable>
);
}
@@ -0,0 +1,88 @@
import {
Divider,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Tooltip,
} from '@chakra-ui/react';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../../ontimeConfig';
export default function EventBlockActionMenu(props) {
const { showAdd, showDelay, showBlock, showClone, actionHandler } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
const blockBtnStyle = {
size: 'sm',
colorScheme: 'blue',
variant: 'outline',
borderRadius: '3px',
};
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...' delay={tooltipDelayMid}>
<MenuButton as={IconButton} aria-label='Options' icon={<FiPlus />}
tabIndex={-1} {...blockBtnStyle} />
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
Add Event after
</MenuItem>
<MenuItem
icon={<IoTimerOutline />}
onClick={() => actionHandler('delay')}
isDisabled={!showDelay}
>
Add Delay after
</MenuItem>
<MenuItem
icon={<FiMinusCircle />}
onClick={() => actionHandler('block')}
isDisabled={!showBlock}
>
Add Block after
</MenuItem>
{showClone && (
<MenuItem
icon={<IoDuplicateOutline />}
onClick={() => actionHandler('clone')}
isDisabled={!showBlock}
>
Clone event
</MenuItem>
)}
<Divider />
<MenuItem
icon={<FiTrash2 />}
onClick={() => actionHandler('delete')}
isDisabled={!showBlock}
color='red.500'
>
Delete event
</MenuItem>
</MenuList>
</Menu>
);
}
EventBlockActionMenu.propTypes = {
showAdd: PropTypes.bool,
showDelay: PropTypes.bool,
showBlock: PropTypes.bool,
showClone: PropTypes.bool,
actionHandler: PropTypes.func,
};
@@ -0,0 +1,90 @@
import { useCallback, useContext } from 'react';
import TimeInput from 'common/components/input/TimeInput';
import { LoggingContext } from 'common/context/LoggingContext';
import { millisToMinutes } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time';
import { validateEntry } from 'common/utils/timesManager';
import PropTypes from 'prop-types';
import style from '../EventBlock.module.scss';
export default function EventBlockTimers(props) {
const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props;
const { emitWarning } = useContext(LoggingContext);
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
const newTime = stringFromMillis(timeStart + delay);
/**
* @description Validates a time input against its pair
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
* @param {number} val - field value
* @return {boolean}
*/
const handleValidation = useCallback(
(field, value) => {
const valid = validateEntry(field, value, timeStart, timeEnd);
if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`);
}
return valid.value;
},
[emitWarning, timeEnd, timeStart]
);
const handleSubmit = useCallback(
(field, value) => {
console.log('called update', field, value)
actionHandler('update', { field, value });
},
[actionHandler]
);
return (
<div className={style.eventTimers}>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={timeStart}
delay={delay}
placeholder='Start'
previousEnd={previousEnd}
/>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={timeEnd}
delay={delay}
placeholder='End'
previousEnd={previousEnd}
/>
<TimeInput
name='duration'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={duration}
delay={delay}
placeholder='Duration'
previousEnd={previousEnd}
/>
{delay !== 0 && delay !== null && (
<div className={style.delayNote}>
{`${delayTime} minutes`}
<br />
{`New start: ${newTime}`}
</div>
)}
</div>
);
}
EventBlockTimers.propTypes = {
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
delay: PropTypes.number,
actionHandler: PropTypes.func,
previousEnd: PropTypes.number,
};
+65 -38
View File
@@ -1,45 +1,75 @@
import { createRef, useCallback, useContext, useEffect } from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import {
defaultPublicAtom,
showQuickEntryAtom,
startTimeIsLastEndAtom,
} from 'common/atoms/LocalEventSettings';
import Empty from 'common/components/state/Empty';
import { CursorContext } from 'common/context/CursorContext';
import { useEventAction } from 'common/hooks/useEventAction';
import { useEventListProvider } from 'common/hooks/useSocketProvider';
import { duplicateEvent } from 'common/utils/eventsManager';
import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types';
import { showQuickEntryAtom } from '../../../common/atoms/LocalEventSettings';
import { CursorContext } from '../../../common/context/CursorContext';
import { useEventListProvider } from '../../../common/hooks/useSocketProvider';
import EntryBlock from '../EntryBlock/EntryBlock';
import useSubscription from '../../../common/context/useSubscription';
import EntryBlock from '../entry-block/EntryBlock';
import EventListItem from './EventListItem';
import style from './List.module.scss';
export default function EventList(props) {
const { events, eventsHandler } = props;
const { cursor, moveCursorUp, moveCursorDown, setCursor, isCursorLocked } =
const { events } = props;
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } =
useContext(CursorContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const { addEvent, reorderEvent } = useEventAction();
const cursorRef = createRef();
const showQuickEntry = useAtomValue(showQuickEntryAtom);
const data = useEventListProvider();
const [selectedId] = useSubscription('selected-id', null);
const [nextId] = useSubscription('next-id', null);
const insertAtCursor = useCallback(
(type, cursor) => {
if (cursor === -1) {
eventsHandler('add', { type: type });
addEvent({ 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 });
const previousEvent = events?.[cursor];
const nextEvent = events?.[cursor + 1];
// prevent adding two non-event blocks consecutively
const isPreviousDifferent = previousEvent?.type !== type;
const isNextDifferent = nextEvent?.type !== type;
if (type === 'clone' && previousEvent) {
const newEvent = duplicateEvent(previousEvent);
newEvent.after = previousEvent.id;
addEvent(newEvent);
} else if (type === 'event') {
const newEvent = {
type: 'event',
after: previousEvent.id,
isPublic: defaultPublic,
};
const options = {
startIsLastEnd: startTimeIsLastEnd ? previousEvent.id : undefined,
};
addEvent(newEvent, options);
} else if (isPreviousDifferent && isNextDifferent) {
addEvent({ type: type, after: previousEvent.id });
}
}
},
[events, eventsHandler]
[addEvent, defaultPublic, events, startTimeIsLastEnd],
);
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
(e) => {
// handle held key
// handle held key
if (e.repeat) return;
// Check if the alt key is pressed
@@ -72,21 +102,21 @@ export default function EventList(props) {
}
}
},
[cursor, events.length, insertAtCursor, moveCursorDown, moveCursorUp]
[cursor, events.length, insertAtCursor, moveCursorDown, moveCursorUp],
);
useEffect(() => {
// attach the event listener
document.addEventListener('keydown', handleKeyPress);
if (cursor > events.length - 1) setCursor(events.length - 1);
if (events.length > 0 && cursor === -1) setCursor(0);
if (cursor > events.length - 1) moveCursorTo(events.length - 1);
if (events.length > 0 && cursor === -1) moveCursorTo(0);
// remove the event listener
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleKeyPress, cursor, events, setCursor]);
}, [handleKeyPress, cursor, events, moveCursorTo]);
// when cursor moves, view should follow
useEffect(() => {
@@ -96,7 +126,6 @@ export default function EventList(props) {
block: 'nearest',
inline: 'start',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cursor]);
// if selected event
@@ -117,10 +146,9 @@ export default function EventList(props) {
}
if (found) {
// move cursor
setCursor(gotoIndex);
moveCursorTo(gotoIndex);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.selectedEventId, isCursorLocked]);
}, [data.selectedEventId, isCursorLocked, moveCursorTo]);
// DND
const handleOnDragEnd = useCallback(
@@ -132,13 +160,9 @@ export default function EventList(props) {
if (result.destination === result.source.index) return;
// Call API
eventsHandler('reorder', {
index: result.draggableId,
from: result.source.index,
to: result.destination.index,
});
reorderEvent(result.draggableId, result.source.index, result.destination.index);
},
[eventsHandler]
[reorderEvent],
);
if (events.length < 1) {
@@ -171,10 +195,11 @@ export default function EventList(props) {
}
const isLast = index === events.length - 1;
return (
<div key={e.id}>
{index === 0 && showQuickEntry && (
<EntryBlock index={e.id} eventsHandler={eventsHandler} />
)}
<div
key={e.id}
className={`${style.bgElement}
${e.type === 'event' && cumulativeDelay !== 0 ? style.delayed : ''}`}
>
<div
ref={cursor === index ? cursorRef : undefined}
className={cursor === index ? style.cursor : ''}
@@ -184,19 +209,17 @@ export default function EventList(props) {
index={index}
eventIndex={eventIndex}
data={e}
selected={data.selectedEventId === e.id}
next={data.nextEventId === e.id}
eventsHandler={eventsHandler}
selected={selectedId === e.id}
next={nextId === e.id}
delay={cumulativeDelay}
previousEnd={previousEnd}
playback={selectedId === e.id ? data.playback : undefined}
/>
</div>
{(showQuickEntry || isLast) && (
{((showQuickEntry && index === cursor) || isLast) && (
<EntryBlock
showKbd={index === cursor}
previousId={e.id}
eventsHandler={eventsHandler}
visible={isLast}
disableAddDelay={e.type === 'delay'}
disableAddBlock={e.type === 'block'}
/>
@@ -212,3 +235,7 @@ export default function EventList(props) {
</div>
);
}
EventList.propTypes = {
events: PropTypes.array,
};
@@ -1,30 +0,0 @@
import { Box } from '@chakra-ui/layout';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
import { CollapseProvider } from '../../../common/context/CollapseContext';
import { CursorProvider } from '../../../common/context/CursorContext';
import { handleLinks } from '../../../common/utils/linkUtils';
import EventListWrapper from './EventListWrapper';
import style from '../Editor.module.scss';
export default function EventListExport() {
return (
<CursorProvider>
<CollapseProvider>
<Box className={style.editor}>
<h1>Event List</h1>
<FiArrowUpRight
className={style.corner}
onClick={(event) => handleLinks(event, 'eventlist')}
/>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</Box>
</CollapseProvider>
</CursorProvider>
);
}
@@ -0,0 +1,26 @@
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { CursorProvider } from 'common/context/CursorContext';
import { handleLinks } from 'common/utils/linkUtils';
import EventListWrapper from './EventListWrapper';
import style from '../Editor.module.scss';
export default function EventListExport() {
return (
<CursorProvider>
<Box className={style.editor}>
<h1>Event List</h1>
<FiArrowUpRight
className={style.corner}
onClick={(event) => handleLinks(event, 'eventlist')}
/>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</Box>
</CursorProvider>
);
}
@@ -1,147 +0,0 @@
import { memo, useCallback, useContext } from 'react';
import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types';
import {
defaultPublicAtom,
startTimeIsLastEndAtom,
} from '../../../common/atoms/LocalEventSettings';
import { LoggingContext } from '../../../common/context/LoggingContext';
import BlockBlock from '../BlockBlock/BlockBlock';
import DelayBlock from '../DelayBlock/DelayBlock';
import EventBlock from '../EventBlock/EventBlock';
const areEqual = (prevProps, nextProps) => {
return (
prevProps.data.revision === nextProps.data.revision &&
prevProps.selected === nextProps.selected &&
prevProps.next === nextProps.next &&
prevProps.index === nextProps.index &&
prevProps.delay === nextProps.delay &&
prevProps.previousEnd === nextProps.previousEnd
);
};
const EventListItem = (props) => {
const { type, index, eventIndex, data, selected, next, eventsHandler, delay, previousEnd } =
props;
const { emitError } = useContext(LoggingContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
/**
* @description calculates duration from given options
* @param {number} start
* @param {number} end
* @returns {number}
*/
const calculateDuration = useCallback(
(start, end) => (start > end ? end + 86400000 - start : end - start),
[]
);
// Create / delete new events
const actionHandler = useCallback(
(action, payload) => {
switch (action) {
case 'event':
eventsHandler(
'add',
{
type: 'event',
after: data.id,
isPublic: defaultPublic,
},
{ startIsLastEnd: startTimeIsLastEnd ? data.id : undefined }
);
break;
case 'delay':
eventsHandler('add', { type: 'delay', after: data.id });
break;
case 'block':
eventsHandler('add', { type: 'block', after: data.id });
break;
case 'delete':
eventsHandler('delete', data.id);
break;
case 'update':
// Handles and filters update requests
const { field, value } = payload;
const newData = { id: data.id };
if (field === 'durationOverride') {
// duration defines timeEnd
newData.timeEnd = data.timeStart += value;
// request update in parent
eventsHandler('patch', newData);
} else if (field === 'timeStart') {
newData.duration = calculateDuration(value, data.timeEnd);
newData.timeStart = value;
// request update in parent
eventsHandler('patch', newData);
} else if (field === 'timeEnd') {
newData.duration = calculateDuration(data.timeStart, value);
newData.timeEnd = value;
// request update in parent
eventsHandler('patch', newData);
} else if (field in data) {
// create object with new field
newData[field] = value;
// request update in parent
eventsHandler('patch', newData);
} else {
emitError(`Unknown field: ${field}`);
}
break;
default:
break;
}
},
[calculateDuration, data, defaultPublic, emitError, eventsHandler, startTimeIsLastEnd]
);
switch (type) {
case 'event':
return (
<EventBlock
index={index}
eventIndex={eventIndex}
data={data}
selected={selected}
next={next}
actionHandler={actionHandler}
delay={delay}
previousEnd={previousEnd}
/>
);
case 'block':
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />;
case 'delay':
return (
<DelayBlock
index={index}
data={data}
eventsHandler={eventsHandler}
actionHandler={actionHandler}
/>
);
default:
break;
}
};
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,
};
@@ -0,0 +1,156 @@
import { useCallback, useContext } from 'react';
import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction';
import { OntimeEvent, OntimeEventEntry } from 'common/models/EventTypes';
import { Playstate } from 'common/models/OntimeTypes';
import { duplicateEvent } from 'common/utils/eventsManager';
import { calculateDuration } from 'common/utils/timesManager';
import { useAtomValue } from 'jotai';
import { CursorContext } from '../../../common/context/CursorContext';
import BlockBlock from '../block-block/BlockBlock';
import DelayBlock from '../delay-block/DelayBlock';
import EventBlock from '../event-block/EventBlock';
export type EventItemActions =
'set-cursor'
| 'event'
| 'delay'
| 'block'
| 'delete'
| 'clone'
| 'update'
interface EventListItemProps {
index: number;
eventIndex: number;
data: OntimeEventEntry;
selected: boolean;
next: boolean;
delay: number;
previousEnd: number;
playback: Playstate;
}
export default function EventListItem(props: EventListItemProps) {
const { index, eventIndex, data, selected, next, delay, previousEnd, playback } = props;
const { emitError } = useContext(LoggingContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const { addEvent, updateEvent, deleteEvent } = useEventAction();
const { moveCursorTo } = useContext(CursorContext);
// Create / delete new events
type FieldValue = {
field: keyof OntimeEvent;
value: unknown;
}
const actionHandler = useCallback(
(action: EventItemActions, payload: number | FieldValue) => {
switch (action) {
case 'set-cursor': {
moveCursorTo(payload as number);
break;
}
case 'event': {
const newEvent = {
type: 'event',
after: data.id,
isPublic: defaultPublic,
};
const options = {
startIsLastEnd: startTimeIsLastEnd ? data.id : undefined,
};
addEvent(newEvent, options);
break;
}
case 'delay': {
addEvent({ type: 'delay', after: data.id });
break;
}
case 'block': {
addEvent({ type: 'block', after: data.id });
break;
}
case 'delete': {
deleteEvent(data.id);
break;
}
case 'clone': {
const newEvent = duplicateEvent(data as OntimeEvent, data.id);
addEvent(newEvent);
break;
}
case 'update': {
// Handles and filters update requests
const { field, value } = payload as FieldValue;
const newData: Partial<OntimeEvent> = { id: data.id };
if (field === 'duration' && data.type === 'event') {
// duration defines timeEnd
newData.timeEnd = data.timeStart += value as number;
updateEvent(newData);
} else if (field === 'timeStart' && data.type === 'event') {
newData.duration = calculateDuration(value as number, data.timeEnd);
newData.timeStart = value as number;
updateEvent(newData);
} else if (field === 'timeEnd' && data.type === 'event') {
newData.duration = calculateDuration(data.timeStart, value as number);
newData.timeEnd = value as number;
updateEvent(newData);
} else if (field in data) {
newData[field] = value as never;
updateEvent(newData);
} else {
emitError(`Unknown field: ${field}`);
}
break;
}
default:
emitError(`Unknown action called: ${action}`);
break;
}
},
[
addEvent,
calculateDuration,
data,
defaultPublic,
deleteEvent,
emitError,
startTimeIsLastEnd,
updateEvent,
],
);
if (data.type === 'event') {
return (
<EventBlock
timeStart={data.timeStart}
timeEnd={data.timeEnd}
duration={data.duration}
index={index}
eventIndex={eventIndex + 1}
eventId={data.id}
isPublic={data.isPublic}
title={data.title}
note={data.note}
delay={delay}
previousEnd={previousEnd}
colour={data.colour}
next={next}
skip={data.skip}
selected={selected}
playback={playback}
actionHandler={actionHandler}
/>
);
} else if (data.type === 'block') {
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />;
} else if (data.type === 'delay') {
return <DelayBlock index={index} data={data} actionHandler={actionHandler} />;
} else {
return null;
}
};
@@ -1,343 +0,0 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { EVENTS_TABLE } from 'common/api/apiConstants';
import {
fetchAllEvents,
requestApplyDelay,
requestDelete,
requestDeleteAll,
requestPatch,
requestPost,
requestPut,
requestReorder,
} from 'common/api/eventsApi.js';
import Empty from 'common/components/state/Empty';
import { useFetch } from 'common/hooks/useFetch.ts';
import EventListMenu from 'features/menu/EventListMenu.jsx';
import { CollapseContext } from '../../../common/context/CollapseContext';
import { LoggingContext } from '../../../common/context/LoggingContext';
import EventList from './EventList';
import styles from '../Editor.module.scss';
export default function EventListWrapper() {
const { expandAll, collapseMultiple } = useContext(CollapseContext);
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const { data, status, isError, refetch } = useFetch(EVENTS_TABLE, fetchAllEvents);
const [events, setEvents] = useState(null);
const addEvent = useMutation(requestPost, {
// we optimistically update here
onMutate: async (newEvent) => {
// cancel ongoing queries
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value
let previousEvents = queryClient.getQueryData(EVENTS_TABLE);
if (previousEvents == null) {
refetch();
previousEvents = queryClient.getQueryData(EVENTS_TABLE);
}
// optimistically update object, temp ID until refetch
const optimistic = [...previousEvents];
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(),
});
queryClient.setQueryData(EVENTS_TABLE, optimistic);
// Return a context with the previous and new todo
return { previousEvents };
},
// Mutation fails, rollback undoes optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(EVENTS_TABLE);
},
});
const updateEvent = useMutation(requestPut, {
// we optimistically update here
onMutate: async (newEvent) => {
// cancel ongoing queries
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
// Return a context with the previous and new todo
return { previousEvent, newEvent };
},
// Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: (newEvent) => {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
},
});
const patchEvent = useMutation(requestPatch, {
// we optimistically update here
onMutate: async (newEvent) => {
// cancel ongoing queries
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
// Return a context with the previous and new todo
return { previousEvent, newEvent };
},
// Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: (newEvent) => {
if (newEvent) {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
} else {
queryClient.invalidateQueries(EVENTS_TABLE);
}
},
});
const deleteEvent = useMutation(requestDelete, {
// we optimistically update here
onMutate: async (eventId) => {
// cancel ongoing queries
queryClient.cancelQueries([EVENTS_TABLE, eventId]);
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
const filtered = [...previousEvents].filter((e) => e.id !== eventId);
// optimistically update object
queryClient.setQueryData(EVENTS_TABLE, filtered);
// Return a context with the previous and new todo
return { previousEvents };
},
// Mutation fails, rollback undos optimist update
onError: (error, eventId, context) => {
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(EVENTS_TABLE);
},
});
const deleteAllEvents = useMutation(requestDeleteAll, {
// we optimistically update here
onMutate: async () => {
// cancel ongoing queries
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
const clear = [];
// optimistically update object
queryClient.setQueryData(EVENTS_TABLE, clear);
// Return a context with the previous and new todo
return { previousEvents };
},
// Mutation fails, rollback undos optimist update
onError: (error, eventId, context) => {
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(EVENTS_TABLE);
},
});
const applyDelay = useMutation(requestApplyDelay, {
// Mutation finished, failed or successful
onSettled: () => {
queryClient.invalidateQueries(EVENTS_TABLE);
},
});
const reorderEvent = useMutation(requestReorder, {
// we optimistically update here
onMutate: async (data) => {
// cancel ongoing queries
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
const e = [...previousEvents];
const [reorderedItem] = e.splice(data.from, 1);
e.splice(data.to, 0, reorderedItem);
// optimistically update object
queryClient.setQueryData(EVENTS_TABLE, e);
// Return a context with the previous and new todo
return { previousEvents };
},
// Mutation fails, rollback undos optimist update
onError: (error, eventId, context) => {
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(EVENTS_TABLE);
},
});
// Show toasts on errors
useEffect(() => {
if (isError) {
emitError('Error fetching data');
}
}, [emitError, isError]);
// Events API
const eventsHandler = useCallback(
async (action, payload, options) => {
switch (action) {
case 'add':
try {
const newEvent = { ...payload };
// there is an option to pass an index of an array to use as start time
if (typeof options?.startIsLastEnd !== 'undefined') {
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
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}`);
}
break;
case 'update':
try {
await updateEvent.mutateAsync(payload);
} catch (error) {
emitError(`Error updating event: ${error.message}`);
}
break;
case 'patch':
try {
await patchEvent.mutateAsync(payload);
} catch (error) {
emitError(`Error updating event: ${error.message}`);
}
break;
case 'delete':
try {
await deleteEvent.mutateAsync(payload);
} catch (error) {
emitError(`Error deleting event: ${error.message}`);
}
break;
case 'reorder':
try {
await reorderEvent.mutateAsync(payload);
} catch (error) {
emitError(`Error re-ordering event: ${error.message}`);
}
break;
case 'applyDelay':
try {
await applyDelay.mutateAsync(payload.id);
} catch (error) {
emitError(`Error applying delay: ${error.message}`);
}
break;
case 'collapseall':
if (data == null) return;
collapseMultiple(data);
break;
case 'expandall':
if (data == null) return;
expandAll();
break;
case 'deleteall':
try {
await deleteAllEvents.mutateAsync();
} catch (error) {
emitError(`Error deleting events: ${error.message}`);
}
break;
default:
emitError(`Unhandled request: ${action}`);
break;
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
);
// Front end should handle bad arguments
useEffect(() => {
if (data == null) return;
setEvents(data.filter((d) => Object.keys(d).length > 0));
}, [data]);
return (
<>
<EventListMenu eventsHandler={eventsHandler} />
<div className={styles.content}>
{status === 'success' && events != null ? (
<EventList events={events} eventsHandler={eventsHandler} />
) : (
<Empty text='Connecting to server' />
)}
</div>
</>
);
}
@@ -0,0 +1,34 @@
import { useContext, useEffect } from 'react';
import Empty from 'common/components/state/Empty';
import { LoggingContext } from 'common/context/LoggingContext';
import EventListMenu from 'features/menu/EventListMenu';
import useEventsList from '../../../common/hooks-query/useEventsList';
import EventList from './EventList';
import styles from '../Editor.module.scss';
export default function EventListWrapper() {
const { emitError } = useContext(LoggingContext);
const { data, status, isError } = useEventsList();
useEffect(() => {
if (isError) {
emitError('Error fetching data');
}
}, [emitError, isError]);
return (
<>
<EventListMenu />
<div className={styles.content}>
{status === 'success' && data ? (
<EventList events={data} />
) : (
<Empty text='Connecting to server' />
)}
</div>
</>
);
}
@@ -23,5 +23,11 @@
.cursor {
box-shadow: 2px 2px 0 $ontime-pink;
border-radius: 14px;
border-radius: 3px;
}
.bgElement {
&.delayed {
background: rgba(214, 158, 46, 0.3);
}
}
@@ -0,0 +1,206 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { Button } from '@chakra-ui/react';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { IoBan } from '@react-icons/all-files/io5/IoBan';
import { editorEventId } from 'common/atoms/LocalEventSettings';
import ColourInput from 'common/components/input/ColourInput';
import TextInput from 'common/components/input/TextInput';
import TimeInput from 'common/components/input/TimeInput';
import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig';
import getDelayTo from 'common/utils/getDelayTo';
import { stringFromMillis } from 'common/utils/time';
import { calculateDuration, validateEntry } from 'common/utils/timesManager';
import { useAtom } from 'jotai';
import useEventsList from '../../common/hooks-query/useEventsList';
import style from './EventEditor.module.scss';
export default function EventEditor() {
const [openId] = useAtom(editorEventId);
const { data } = useEventsList();
const { emitWarning, emitError } = useContext(LoggingContext);
const { updateEvent } = useEventAction();
const [event, setEvent] = useState(null);
const [delay, setDelay] = useState(0);
useEffect(() => {
if (!data || !openId) {
return;
}
const eventIndex = data.findIndex((event) => event.id === openId);
if (eventIndex > -1) {
setDelay(getDelayTo(data, eventIndex));
setEvent(data[eventIndex]);
}
}, [data, event, openId]);
const handleSubmit = useCallback(
(field, value) => {
const newEventData = { id: event.id };
switch (field) {
case 'durationOverride': {
// duration defines timeEnd
newEventData.timeEnd = event.timeStart += value;
break;
}
case 'timeStart': {
newEventData.duration = calculateDuration(value, event.timeEnd);
newEventData.timeStart = value;
break;
}
case 'timeEnd': {
newEventData.duration = calculateDuration(event.timeStart, value);
newEventData.timeEnd = value;
break;
}
default: {
if (field in event) {
// create object with new field
newEventData[field] = value;
break;
} else {
emitError(`Unknown field: ${field}`);
return;
}
}
}
updateEvent(newEventData);
},
[emitError, event, updateEvent],
);
const timerValidationHandler = useCallback(
(entry, val) => {
const valid = validateEntry(entry, val, event.timeStart, event.timeEnd);
if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`);
}
return valid.value;
},
[emitWarning, event?.timeStart, event?.timeEnd],
);
const togglePublic = useCallback(
(currentValue) => {
updateEvent({ id: event.id, isPublic: !currentValue });
},
[event?.id, updateEvent],
);
if (!event) {
return <span>Loading</span>;
}
const delayed = delay !== 0;
const addedTime = delayed
? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes`
: null;
const newStart = delayed ? `New start ${stringFromMillis(event.timeStart + delay)}` : null;
const newEnd = delayed ? `New end ${stringFromMillis(event.timeEnd + delay)}` : null;
return (
<div className={style.eventEditor}>
<div className={style.timers}>
<label className={style.inputLabel}>
Start time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newStart}</div>}
</label>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.timeStart}
delay={0}
placeholder='Start'
/>
<label className={style.inputLabel}>
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newEnd}</div>}
</label>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.timeEnd}
delay={0}
placeholder='End'
/>
<label className={style.inputLabel}>Duration</label>
<TimeInput
name='duration'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.duration}
delay={0}
placeholder='Duration'
/>
</div>
<div className={style.titles}>
<div className={style.left}>
<div className={style.column}>
<label className={style.inputLabel}>Title</label>
<TextInput field='title' initialText={event.title} submitHandler={handleSubmit} />
</div>
<div className={style.column}>
<label className={style.inputLabel}>Subtitle</label>
<TextInput field='subtitle' initialText={event.subtitle} submitHandler={handleSubmit} />
</div>
<div className={style.column}>
<label className={style.inputLabel}>Presenter</label>
<TextInput
field='presenter'
initialText={event.presenter}
submitHandler={handleSubmit}
/>
</div>
<div className={style.padTop}>
<Button
leftIcon={<FiUsers />}
size='sm'
colorScheme='blue'
variant={event.isPublic ? 'solid' : 'outline'}
onClick={() => togglePublic(event.isPublic)}
>
{event.isPublic ? 'Event is Public' : 'Make event public'}
</Button>
</div>
</div>
<div className={style.right}>
<div className={style.column}>
<label className={style.inputLabel}>Colour</label>
<div className={style.inline}>
<ColourInput
value={event?.colour}
handleChange={(value) => handleSubmit('colour', value)}
/>
<Button
leftIcon={<IoBan />}
onClick={() => handleSubmit('colour', '')}
variant='outline'
colorScheme='blue'
borderRadius='3px'
size='sm'
>
Clear colour
</Button>
</div>
</div>
<div className={style.column}>
<label className={style.inputLabel}>Notes</label>
<TextInput
field='note'
initialText={event.note}
submitHandler={handleSubmit}
isTextArea
/>
</div>
</div>
<div className={style.osc}>{`OSC Trigger /ontime/gotoid/${event.id}`}</div>
</div>
</div>
);
}
@@ -0,0 +1,87 @@
@use '../../theme/main' as *;
.eventEditor {
overflow-y: scroll;
max-height: 33vh;
gap: max(16px, 2vh);
display: grid;
grid-template-areas: 'timers titles';
grid-template-columns: auto 1fr;
.timers,
.titles {
background-color: $bg-gray-1100;
border-radius: 2px;
padding: 8px 16px;
}
.timers {
grid-area: timers;
display: flex;
flex-direction: column;
gap: 8px;
}
.titles {
grid-area: titles;
display: grid;
grid-template-areas:
'left right'
'tag tag';
grid-template-columns: 1fr 1fr;
.left,
.right {
display: flex;
flex-direction: column;
gap: 8px;
}
.left {
grid-area: left;
padding-right: 16px;
}
.right {
padding-left: 16px;
grid-area: right;
border-left: 1px solid $bg-gray-900;
}
.osc {
grid-area: tag;
font-size: 14px;
color: $ontime-accent;
text-align: right;
user-select: text;
}
}
.inline {
display: flex;
align-items: center;
gap: 16px;
}
.column {
display: flex;
flex-direction: column;
gap: 8px;
}
.padTop {
margin-top: 8px;
}
.inputLabel {
font-size: 13px;
display: block;
color: $label-gray;
.delayLabel {
color: $text-delay;
}
}
}
@@ -0,0 +1,40 @@
import { Box, IconButton } from '@chakra-ui/react';
import { FiX } from '@react-icons/all-files/fi/FiX';
import { editorEventId } from 'common/atoms/LocalEventSettings';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { useAtom } from 'jotai';
import EventEditor from './EventEditor';
import style from '../editors/Editor.module.scss';
/* Styling for action buttons */
const closeBtnStyle = {
size: 'md',
variant: 'ghost',
colorScheme: 'white',
_hover: { bg: '#ebedf0', color: '#333' },
};
export default function InfoExport() {
const [openId, setOpenId] = useAtom(editorEventId);
return (
<Box className={`${style.eventEditor} ${!openId ? style.noEvent : ''}`}>
<ErrorBoundary>
<div className={style.header}>
<h1>Event Editor</h1>
<IconButton
aria-label='Close Menu'
icon={<FiX />}
onClick={() => setOpenId(null)}
{...closeBtnStyle}
/>
</div>
<div className={style.content}>
<EventEditor />
</div>
</ErrorBoundary>
</Box>
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Box } from '@chakra-ui/layout';
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import ErrorBoundary from '../../common/components/errorBoundary/ErrorBoundary';
+2 -6
View File
@@ -1,17 +1,13 @@
import { useState } from 'react';
import { APP_TABLE } from 'common/api/apiConstants';
import { getInfo, ontimePlaceholderInfo } from 'common/api/ontimeApi';
import { useFetch } from 'common/hooks/useFetch';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import useInfo from '../../common/hooks-query/useInfo';
import { openLink } from '../../common/utils/linkUtils';
import style from './Info.module.scss';
export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, {
placeholderData: ontimePlaceholderInfo,
});
const { data, status } = useInfo();
const [collapsed, setCollapsed] = useState(false);
const baseURL = 'http://__IP__:4001';
+19 -48
View File
@@ -1,34 +1,40 @@
import { memo, useCallback, useContext } from 'react';
import { ButtonGroup, HStack } from '@chakra-ui/react';
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
import PropTypes from 'prop-types';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import { CursorContext } from '../../common/context/CursorContext';
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
import { CursorContext } from 'common/context/CursorContext';
import { useEventAction } from 'common/hooks/useEventAction';
import MenuActionButtons from './MenuActionButtons';
import style from './EventListMenu.module.css';
const EventListMenu = ({ eventsHandler }) => {
const cursorBtnProps = {
size: 'sm',
color: 'pink.300',
borderColor: 'pink.300',
variant: 'outline',
_hover: { bg: 'pink.400', color: 'white' },
};
const EventListMenu = () => {
const { isCursorLocked, toggleCursorLocked, moveCursorUp, moveCursorDown } =
useContext(CursorContext);
const { addEvent, deleteAllEvents } = useEventAction();
const actionHandler = useCallback(
(action) => {
switch (action) {
case 'event':
eventsHandler('add', { type: action });
addEvent({ type: action });
break;
case 'delay':
eventsHandler('add', { type: action });
addEvent({ type: action });
break;
case 'block':
eventsHandler('add', { type: action });
addEvent({ type: action });
break;
case 'cursorUp':
moveCursorUp();
@@ -40,59 +46,29 @@ const EventListMenu = ({ eventsHandler }) => {
toggleCursorLocked();
break;
case 'deleteall':
eventsHandler('deleteall');
deleteAllEvents();
break;
default:
break;
}
},
[eventsHandler, moveCursorDown, moveCursorUp, toggleCursorLocked]
[addEvent, deleteAllEvents, 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>
<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', color: 'white' }}
/>
<TooltipActionBtn
{...cursorBtnProps}
clickHandler={() => actionHandler('cursorDown')}
icon={<IoCaretDown />}
tooltip='Move cursor down Alt + ↓'
_hover={{ bg: 'pink.400', color: 'white' }}
/>
<TooltipActionBtn
{...cursorBtnProps}
@@ -101,8 +77,7 @@ const EventListMenu = ({ eventsHandler }) => {
tooltip='Lock cursor to current'
width='3em'
backgroundColor={isCursorLocked && 'pink.400'}
color={isCursorLocked && 'white'}
_hover={{ bg: 'pink.400', color: 'white' }}
color={isCursorLocked ? 'white' : 'pink.400'}
variant={isCursorLocked ? 'solid' : 'outline'}
/>
</ButtonGroup>
@@ -112,7 +87,3 @@ const EventListMenu = ({ eventsHandler }) => {
};
export default memo(EventListMenu);
EventListMenu.propTypes = {
eventsHandler: PropTypes.func,
};
@@ -1,15 +1,29 @@
import { IconButton } from '@chakra-ui/button';
import { Divider } from '@chakra-ui/layout';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { Tooltip } from '@chakra-ui/tooltip';
import {
IconButton,
Divider,
Menu,
MenuButton,
MenuItem,
MenuList,
Tooltip,
} from '@chakra-ui/react';
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 { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
import PropTypes from 'prop-types';
import { useEventAction } from 'common/hooks/useEventAction';
import { Size } from 'common/models/UtilTypes';
export default function MenuActionButtons(props) {
interface MenuActionButtonsProps {
actionHandler: (action: string) => void;
size?: Size;
}
// Todo: add useEventActionsHook
export default function MenuActionButtons(props: MenuActionButtonsProps) {
const { actionHandler, size = 'xs' } = props;
const { deleteAllEvents } = useEventAction();
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
@@ -23,10 +37,7 @@ export default function MenuActionButtons(props) {
aria-label='Create Menu'
size={size}
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
backgroundColor='orange.200'
color='orange.500'
colorScheme='blue'
/>
</Tooltip>
<MenuList style={menuStyle}>
@@ -40,7 +51,7 @@ export default function MenuActionButtons(props) {
Add Block first
</MenuItem>
<Divider />
<MenuItem icon={<FiTrash2 />} onClick={() => actionHandler('deleteall')} color='red.500'>
<MenuItem icon={<FiTrash2 />} onClick={() => deleteAllEvents()} color='red.500'>
Delete All
</MenuItem>
</MenuList>
@@ -48,7 +59,3 @@ export default function MenuActionButtons(props) {
);
}
MenuActionButtons.propTypes = {
actionHandler: PropTypes.func,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
};
@@ -1,11 +1,17 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { vi } from 'vitest';
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
import MenuActionButtons from '../MenuActionButtons';
const actionHandler = vi.fn();
const renderInMock = () => {
render(<MenuActionButtons actionHandler={actionHandler} />);
render(
<QueryClientProvider client={queryClientMock}>
<MenuActionButtons actionHandler={actionHandler} />
</QueryClientProvider>,
);
};
test('check that menu bar renders correctly', () => {
+34 -34
View File
@@ -1,18 +1,14 @@
/* eslint-disable jsx-a11y/anchor-has-content */
import { useCallback, useContext, useEffect, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
import { Tooltip } from '@chakra-ui/tooltip';
import { Button, IconButton, Input, ModalBody, Tooltip } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { ALIASES } from 'common/api/apiConstants';
import { useFetch } from 'common/hooks/useFetch';
import { viewerLocations } from '../../appConstants';
import { getAliases, postAliases } from '../../common/api/ontimeApi';
import { postAliases } from '../../common/api/ontimeApi';
import { LoggingContext } from '../../common/context/LoggingContext';
import useAliases from '../../common/hooks-query/useAliases';
import { validateAlias } from '../../common/utils/aliases';
import { handleLinks, host } from '../../common/utils/linkUtils';
import { tooltipDelayFast } from '../../ontimeConfig';
@@ -22,7 +18,7 @@ import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
export default function AliasesModal() {
const { data, status, refetch } = useFetch(ALIASES, getAliases);
const { data, status, refetch } = useAliases();
const { emitError } = useContext(LoggingContext);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
@@ -68,14 +64,18 @@ export default function AliasesModal() {
setAliases(validatedAliases);
if (!errors) {
await postAliases(aliases);
await refetch();
setChanged(false);
try {
await postAliases(aliases);
} catch (error) {
emitError(`Error saving settings: ${error}`);
} finally {
await refetch();
setChanged(false);
}
}
setSubmitting(false);
},
[aliases, refetch]
[aliases, refetch],
);
/**
@@ -156,7 +156,7 @@ export default function AliasesModal() {
setAliases(temp);
setChanged(true);
},
[aliases]
[aliases],
);
return (
@@ -194,16 +194,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 />
@@ -212,16 +212,16 @@ 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>
@@ -1,26 +1,26 @@
import { useContext, useEffect, useState } from 'react';
import { IconButton } from '@chakra-ui/button';
import { ModalBody } from '@chakra-ui/modal';
import {
Checkbox,
FormControl,
FormLabel,
IconButton,
Input,
ModalBody,
PinInput,
PinInputField,
Select,
} from '@chakra-ui/react';
import { FiEye } from '@react-icons/all-files/fi/FiEye';
import { FiX } from '@react-icons/all-files/fi/FiX';
import { APP_SETTINGS } from 'common/api/apiConstants';
import { getSettings, ontimePlaceholderSettings, postSettings } from 'common/api/ontimeApi';
import { useFetch } from 'common/hooks/useFetch';
import { postSettings } from 'common/api/ontimeApi';
import { useAtom } from 'jotai';
import { version } from '../../../package.json';
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import { LoggingContext } from '../../common/context/LoggingContext';
import useSettings from '../../common/hooks-query/useSettings';
import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings.type';
import { inputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -28,7 +28,7 @@ import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
export default function AppSettingsModal() {
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
const { data, status, refetch } = useSettings();
const { emitError, emitWarning } = useContext(LoggingContext);
const [formData, setFormData] = useState(ontimePlaceholderSettings);
const [changed, setChanged] = useState(false);
@@ -226,7 +226,7 @@ export default function AppSettingsModal() {
setChanged(true);
}}
>
Show quick entry on hover
Show quick entry on cursor
</Checkbox>
<Checkbox
isChecked={formSettings.startTimeIsLastEnd}
@@ -1,12 +1,10 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
import { EVENT_TABLE } from 'common/api/apiConstants';
import { fetchEvent, postEvent } from 'common/api/eventApi';
import { useFetch } from 'common/hooks/useFetch';
import { FormLabel, Input, ModalBody, Textarea } from '@chakra-ui/react';
import { postEvent } from 'common/api/eventApi';
import { eventPlaceholderSettings } from '../../common/api/ontimeApi';
import { LoggingContext } from '../../common/context/LoggingContext';
import useEvent from '../../common/hooks-query/useEvent';
import { eventDataPlaceholder } from '../../common/models/EventData.type';
import { inputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -14,9 +12,9 @@ import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
export default function SettingsModal() {
const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent);
const { data, status, refetch } = useEvent();
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(eventPlaceholderSettings);
const [formData, setFormData] = useState(eventDataPlaceholder);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
@@ -1,12 +1,11 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
import { FormControl, FormLabel, Input, ModalBody, Switch } from '@chakra-ui/react';
import { FiInfo } from '@react-icons/all-files/fi/FiInfo';
import { APP_TABLE } from 'common/api/apiConstants';
import { getInfo, httpPlaceholder, ontimeVars } from 'common/api/ontimeApi';
import { useFetch } from 'common/hooks/useFetch';
import { LoggingContext } from '../../common/context/LoggingContext';
import useInfo from '../../common/hooks-query/useInfo';
import { httpPlaceholder } from '../../common/models/Http.type';
import { ontimeVars } from '../../common/models/OntimeVars';
import { inputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -14,7 +13,7 @@ import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
export default function IntegrationSettingsModal() {
const { data, status, refetch } = useFetch(APP_TABLE, getInfo);
const { data, status, refetch } = useInfo();
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(httpPlaceholder);
const [changed, setChanged] = useState(false);
@@ -55,7 +54,7 @@ export default function IntegrationSettingsModal() {
setSubmitting(false);
}
},
[emitError, formData]
[emitError, formData],
);
/**
@@ -97,18 +96,18 @@ export default function IntegrationSettingsModal() {
</span>
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Variable
</td>
<td className={style.labelNote}>Value</td>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Variable
</td>
<td className={style.labelNote}>Value</td>
</tr>
{ontimeVars.map((v) => (
<tr key={v.name}>
<td className={style.labelNote}>{v.name}</td>
<td>{v.description}</td>
</tr>
{ontimeVars.map((v) => (
<tr key={v.name}>
<td className={style.labelNote}>{v.name}</td>
<td>{v.description}</td>
</tr>
))}
))}
</tbody>
</table>
</div>
+6 -2
View File
@@ -4,8 +4,12 @@ import {
ModalContent,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/modal';
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/tabs';
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from '@chakra-ui/react';
import PropTypes from 'prop-types';
import AliasesModal from './AliasesModal';
+12 -1
View File
@@ -14,7 +14,6 @@
}
.modalFields {
min-height: 45vh;
max-height: 45vh;
overflow-y: auto;
scrollbar-color: rgba($light-bg, 0.35) rgba($light-bg, 0.15);
@@ -110,6 +109,18 @@
}
//////////////////////////////////// notes
ul.featureList {
li {
display: flex;
align-items: center;
svg {
color: $ontime-accent-text;
margin-right: 4px;
}
}
}
p {
&.notes {
text-align: center;
@@ -1,13 +1,12 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { OSC_SETTINGS } from 'common/api/apiConstants';
import { getOSC, oscPlaceholderSettings, postOSC } from 'common/api/ontimeApi';
import { useFetch } from 'common/hooks/useFetch';
import { postOSC } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn';
import { LoggingContext } from '../../common/context/LoggingContext';
import useOscSettings from '../../common/hooks-query/useOscSettings';
import { oscPlaceholderSettings } from '../../common/models/OscSettings.type';
import { inputProps, portInputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -77,7 +76,7 @@ const oscTriggerEndpoints = [
];
export default function OscSettingsModal() {
const { data, status, refetch } = useFetch(OSC_SETTINGS, getOSC);
const { data, status, refetch } = useOscSettings();
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings);
const [changed, setChanged] = useState(false);
@@ -1,4 +1,4 @@
import { Button } from '@chakra-ui/button';
import { Button } from '@chakra-ui/react';
import PropTypes from 'prop-types';
import style from './Modals.module.scss';
@@ -1,12 +1,11 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
import { Input, ModalBody } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { USERFIELDS } from 'common/api/apiConstants';
import { useFetch } from 'common/hooks/useFetch';
import { getUserFields, postUserFields, userFieldsPlaceholder } from '../../common/api/ontimeApi';
import { postUserFields } from '../../common/api/ontimeApi';
import { LoggingContext } from '../../common/context/LoggingContext';
import useUserFields from '../../common/hooks-query/useUserFields';
import { userFieldsPlaceholder } from '../../common/models/UserFields.type';
import { handleLinks, host } from '../../common/utils/linkUtils';
import SubmitContainer from './SubmitContainer';
@@ -14,7 +13,7 @@ import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
export default function TableOptionsModal() {
const { data, status, refetch } = useFetch(USERFIELDS, getUserFields);
const { data, status, refetch } = useUserFields();
const { emitError } = useContext(LoggingContext);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
@@ -1,13 +1,13 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel } from '@chakra-ui/react';
import { FormControl, FormLabel, ModalBody } from '@chakra-ui/react';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { VIEW_SETTINGS } from '../../common/api/apiConstants';
import { getView, postView, viewsPlaceholder } from '../../common/api/ontimeApi';
import { postView } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn';
import { LoggingContext } from '../../common/context/LoggingContext';
import { useFetch } from '../../common/hooks/useFetch';
import useSettings from '../../common/hooks-query/useSettings';
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
import { openLink } from '../../common/utils/linkUtils';
import SubmitContainer from './SubmitContainer';
@@ -15,9 +15,9 @@ import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
export default function ViewsSettingsModal() {
const { data, status, refetch } = useFetch(VIEW_SETTINGS, getView);
const { data, status, refetch } = useSettings();
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(viewsPlaceholder);
const [formData, setFormData] = useState(viewsSettingsPlaceholder);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
@@ -90,6 +90,16 @@ export default function ViewsSettingsModal() {
This feature allows user defined CSS to override the application stylesheets as a way to
customise viewers appearance.
<br />
Currently the feature affects the following views<br />
<ul className={style.featureList}>
<li><IoCheckmarkSharp /> Stage timer</li>
<li><IoCheckmarkSharp /> Clock</li>
<li><IoCheckmarkSharp /> Minimal timer</li>
<li><IoCheckmarkSharp /> Backstage screen</li>
<li><IoCheckmarkSharp /> Public screen</li>
<li><IoCheckmarkSharp /> Picture in Picture</li>
<li><IoCheckmarkSharp /> Countdown</li>
</ul>
Read more about it in the documentation{' '}
<a
href='#!'
+1 -1
View File
@@ -1,6 +1,6 @@
import { useCallback, useContext, useEffect, useMemo } from 'react';
import { useBlockLayout, useColumnOrder, useResizeColumns, useTable } from 'react-table';
import { Tooltip } from '@chakra-ui/tooltip';
import { Tooltip } from '@chakra-ui/react';
import {
closestCenter,
DndContext,
+3 -6
View File
@@ -1,6 +1,5 @@
import { useContext } from 'react';
import { Divider } from '@chakra-ui/layout';
import { Tooltip } from '@chakra-ui/tooltip';
import { Divider, Tooltip } from '@chakra-ui/react';
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
import { IoContract } from '@react-icons/all-files/io5/IoContract';
@@ -8,12 +7,10 @@ import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import { IoMoon } from '@react-icons/all-files/io5/IoMoon';
import PropTypes from 'prop-types';
import { EVENT_TABLE } from '../../common/api/apiConstants';
import { fetchEvent } from '../../common/api/eventApi';
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
import { useFetch } from '../../common/hooks/useFetch';
import useFullscreen from '../../common/hooks/useFullscreen';
import { useTimerProvider } from '../../common/hooks/useSocketProvider';
import useEvent from '../../common/hooks-query/useEvent';
import { formatDisplay } from '../../common/utils/dateConfig';
import { formatTime } from '../../common/utils/time';
import { tooltipDelayFast } from '../../ontimeConfig';
@@ -27,7 +24,7 @@ export default function TableHeader({handleCSVExport, featureData}) {
useContext(TableSettingsContext);
const timer = useTimerProvider();
const { isFullScreen, toggleFullScreen } = useFullscreen();
const { data: event } = useFetch(EVENT_TABLE, fetchEvent);
const { data: event } = useEvent();
const selected = !featureData.numEvents
? 'No events'
+7 -8
View File
@@ -1,12 +1,11 @@
import { useCallback, useContext,useEffect } from 'react';
import { useCallback, useContext, useEffect } from 'react';
import { EVENTS_TABLE, USERFIELDS } from '../../common/api/apiConstants';
import { fetchAllEvents, requestPatch } from '../../common/api/eventsApi';
import { getUserFields } from '../../common/api/ontimeApi';
import { requestPatchEvent } from '../../common/api/eventsApi';
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
import { useFetch } from '../../common/hooks/useFetch';
import useMutateEvents from '../../common/hooks/useMutateEvents';
import { useCuesheetProvider } from '../../common/hooks/useSocketProvider';
import useEventsList from '../../common/hooks-query/useEventsList';
import useUserFields from '../../common/hooks-query/useUserFields';
import OntimeTable from './OntimeTable';
import TableHeader from './TableHeader';
@@ -15,9 +14,9 @@ import { makeCSV, makeTable } from './utils';
import style from './Table.module.scss';
export default function TableWrapper() {
const { data: events } = useFetch(EVENTS_TABLE, fetchAllEvents);
const { data: userFields } = useFetch(USERFIELDS, getUserFields);
const mutation = useMutateEvents(requestPatch);
const { data: events } = useEventsList();
const { data: userFields } = useUserFields();
const mutation = useMutateEvents(requestPatchEvent);
const { theme } = useContext(TableSettingsContext);
const featureData = useCuesheetProvider();
@@ -1,4 +1,4 @@
import { Tooltip } from '@chakra-ui/tooltip';
import { Tooltip } from '@chakra-ui/react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
@@ -1,4 +1,4 @@
import { Tooltip } from '@chakra-ui/tooltip';
import { Tooltip } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import PropTypes from 'prop-types';
@@ -1,4 +1,4 @@
import { Button } from '@chakra-ui/button';
import { Button } from '@chakra-ui/react';
import PropTypes from 'prop-types';
import style from '../Table.module.scss';
+9 -16
View File
@@ -1,24 +1,17 @@
/* eslint-disable react/display-name */
import { useEffect, useMemo, useState } from 'react';
import { EVENT_TABLE, EVENTS_TABLE } from '../../common/api/apiConstants';
import { fetchEvent } from '../../common/api/eventApi';
import { fetchAllEvents } from '../../common/api/eventsApi';
import { useSocket } from '../../common/context/socketContext';
import { useFetch } from '../../common/hooks/useFetch';
import { getView } from '../../common/api/ontimeApi';
import useSubscription from '../../common/context/useSubscription';
import { eventPlaceholderSettings } from '../../common/api/ontimeApi';
import useEvent from '../../common/hooks-query/useEvent';
import useEventsList from '../../common/hooks-query/useEventsList';
import useViewSettings from '../../common/hooks-query/useViewSettings';
const withSocket = (Component) => {
return (props) => {
const { data: eventsData } = useFetch(EVENTS_TABLE, fetchAllEvents, {
placeholderData: [],
});
const { data: genData } = useFetch(EVENT_TABLE, fetchEvent, {
placeholderData: eventPlaceholderSettings,
});
const { data: viewSettings } = useFetch(VIEW_SETTINGS, getView);
const { data: eventsData } = useEventsList();
const { data: genData } = useEvent();
const { data: viewSettings } = useViewSettings();
const socket = useSocket();
const [pres, setPres] = useState({
@@ -145,7 +138,7 @@ const withSocket = (Component) => {
};
/******************************************/
/*** + timeManager ***/
/*** + TimeManagerType ***/
/*** WRAP INFORMATION RELATED TO TIME ***/
/*** -------------------------------- ***/
/******************************************/
@@ -153,7 +146,7 @@ const withSocket = (Component) => {
// inject info:
// is timer finished
// get clock string
const timeManager = {
const TimeManagerType = {
...timer,
finished: playback === 'start' && timer.isNegative && timer.startedAt,
playstate: playback,
@@ -173,7 +166,7 @@ const withSocket = (Component) => {
lower={lower}
title={titleManager}
publicTitle={publicTitleManager}
time={timeManager}
time={TimeManagerType}
events={publicEvents}
backstageEvents={eventsData}
selectedId={selectedId}
@@ -7,7 +7,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import Paginator from '../../../common/components/views/Paginator';
import Paginator from '../../../common/components/paginator/Paginator';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
import { formatTime } from '../../../common/utils/time';
+4 -6
View File
@@ -4,18 +4,16 @@ import { useSearchParams } from 'react-router-dom';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import NavLogo from '../../../common/components/nav/NavLogo';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import {
TimeManager,
ViewSettings,
} from '../../../common/models/OntimeTypes';
import { TimeManagerType } from '../../../common/models/TimeManaget.type';
import { ViewSettingsType } from '../../../common/models/ViewSettings.type';
import { OverridableOptions } from '../../../common/models/ViewTypes';
import { formatTime } from '../../../common/utils/time';
import './Clock.scss';
interface ClockProps {
time: TimeManager;
viewSettings: ViewSettings;
time: TimeManagerType;
viewSettings: ViewSettingsType;
}
const formatOptions = {
@@ -1,15 +1,13 @@
import { memo, useEffect, useState } from 'react';
import isEqual from 'react-fast-compare';
import { useSearchParams } from 'react-router-dom';
import { overrideStylesURL } from 'common/api/apiConstants';
import { useRuntimeStylesheet } from 'common/hooks/useRuntimeStylesheet';
import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import LowerClean from './LowerClean';
import LowerLines from './LowerLines';
const isEqual = require('react-fast-compare');
const areEqual = (prevProps, nextProps) => {
return isEqual(prevProps.title, nextProps.title) && isEqual(prevProps.lower, nextProps.lower);
};
@@ -4,20 +4,18 @@ import { useSearchParams } from 'react-router-dom';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import NavLogo from '../../../common/components/nav/NavLogo';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import {
PresenterMessageData,
TimeManager,
ViewSettings,
} from '../../../common/models/OntimeTypes';
import { PresenterMessageType } from '../../../common/models/PresenterMessage.type';
import { TimeManagerType } from '../../../common/models/TimeManaget.type';
import { ViewSettingsType } from '../../../common/models/ViewSettings.type';
import { OverridableOptions } from '../../../common/models/ViewTypes';
import { formatDisplay } from '../../../common/utils/dateConfig';
import './MinimalTimer.scss';
interface MinimalTimerProps {
pres: PresenterMessageData;
time: TimeManager;
viewSettings: ViewSettings;
pres: PresenterMessageType;
time: TimeManagerType;
viewSettings: ViewSettingsType;
}
export default function MinimalTimer(props: MinimalTimerProps) {
@@ -7,7 +7,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import Paginator from '../../../common/components/views/Paginator';
import Paginator from '../../../common/components/paginator/Paginator';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { formatTime } from '../../../common/utils/time';
@@ -6,7 +6,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import Paginator from '../../../common/components/views/Paginator';
import Paginator from '../../../common/components/paginator/Paginator';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { formatTime } from '../../../common/utils/time';
import { titleVariants } from '../common/animation';