mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 03:13:47 +00:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-9
@@ -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;
|
||||
}
|
||||
+41
-19
@@ -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;
|
||||
}
|
||||
+46
-32
@@ -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,
|
||||
};
|
||||
@@ -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,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,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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
+22
-15
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
Reference in New Issue
Block a user