mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
Refact/feature sockets (#193)
* chore: migrate and upgrade state dependencies * refactor(eventlist): get data from store * refactor(messagecontrol): get data from store * refactor(playbackcontrol): get data from store * refactor(info): get data from store * refactor(messagecontrol): handle mutations in hook * feat(playbackcontrol): get data from store * refactor(playbackcontrol): handle mutations in hook * refactor(playbackcontrol): simplify object * refactor: simplify interfaces * refactor(InputRow): mixed control + styles * refactor(socket): cleanup unused endpoints * refactor(table): prepare table socket endpoint * refactor(timer): extract timer to own endpoint * refactor(table): get data from store * chore: update tests * refactor(socket): update sockets on cycle * refactor: remove unnecessary safeguards
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import SocketProvider from 'common/context/socketContext';
|
||||
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
import MessageControl from '../message/MessageControl';
|
||||
|
||||
|
||||
// need to inject the socket provider to make component
|
||||
// render without failing
|
||||
const MockMessageControl = () => {
|
||||
return (
|
||||
<SocketProvider>
|
||||
<MessageControl />
|
||||
</SocketProvider>
|
||||
<QueryClientProvider client={queryClientMock}>
|
||||
<SocketProvider>
|
||||
<MessageControl />
|
||||
</SocketProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import SocketProvider from 'common/context/socketContext';
|
||||
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
import PlaybackControl from '../playback/PlaybackControl';
|
||||
|
||||
|
||||
test('check that playback control renders', async () => {
|
||||
// need to inject the socket provider to make component
|
||||
// render without failing
|
||||
render(
|
||||
<SocketProvider>
|
||||
<PlaybackControl />
|
||||
</SocketProvider>
|
||||
<QueryClientProvider client={queryClientMock}>
|
||||
<SocketProvider>
|
||||
<PlaybackControl />
|
||||
</SocketProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
// Text labels for times
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
@@ -11,14 +11,27 @@ import style from './MessageControl.module.scss';
|
||||
|
||||
export default function InputRow(props) {
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
const [inputText, setInputText] = useState(text || '');
|
||||
|
||||
const handleInputChange = (newValue) => {
|
||||
setInputText(newValue);
|
||||
changeHandler(newValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (text) {
|
||||
setInputText(text);
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${visible ? style.inputRowActive: ''}`}>
|
||||
<span className={style.label}>{label}</span>
|
||||
<div className={style.inputItems}>
|
||||
<Editable
|
||||
onChange={(event) => changeHandler(event)}
|
||||
value={text}
|
||||
onChange={(newValue) => handleInputChange(newValue)}
|
||||
value={inputText}
|
||||
placeholder={placeholder}
|
||||
className={style.inline}
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
|
||||
import { useMessageControlProvider } from '../../../common/hooks/useSocketProvider';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import InputRow from './InputRow';
|
||||
@@ -12,88 +12,7 @@ import InputRow from './InputRow';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
export default function MessageControl() {
|
||||
const socket = useSocket();
|
||||
const [pres, setPres] = useState({
|
||||
text: '',
|
||||
visible: false,
|
||||
});
|
||||
const [publ, setPubl] = useState({
|
||||
text: '',
|
||||
visible: false,
|
||||
});
|
||||
const [lower, setLower] = useState({
|
||||
text: '',
|
||||
visible: false,
|
||||
});
|
||||
const [onAir, setonAir] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Handle timer messages
|
||||
socket.on('messages-timer', (data) => {
|
||||
setPres({ ...data });
|
||||
});
|
||||
|
||||
// Handle public messages
|
||||
socket.on('messages-public', (data) => {
|
||||
setPubl({ ...data });
|
||||
});
|
||||
|
||||
// Handle lower third messages
|
||||
socket.on('messages-lower', (data) => {
|
||||
setLower({ ...data });
|
||||
});
|
||||
|
||||
// Handle lower third messages
|
||||
socket.on('onAir', (data) => {
|
||||
setonAir(data);
|
||||
});
|
||||
|
||||
// Ask for up to date data
|
||||
socket.emit('get-messages');
|
||||
// Ask for onAir state
|
||||
socket.emit('get-onAir');
|
||||
|
||||
// Clear listeners
|
||||
return () => {
|
||||
socket.off('messages-public');
|
||||
socket.off('messages-timer');
|
||||
socket.off('messages-lower');
|
||||
socket.off('onAir');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const messageControl = useCallback(
|
||||
(action, payload) => {
|
||||
switch (action) {
|
||||
case 'pres-text':
|
||||
socket.emit('set-timer-message-text', payload);
|
||||
break;
|
||||
case 'toggle-pres-visible':
|
||||
socket.emit('set-timer-message-visible', payload);
|
||||
break;
|
||||
case 'publ-text':
|
||||
socket.emit('set-public-message-text', payload);
|
||||
break;
|
||||
case 'toggle-publ-visible':
|
||||
socket.emit('set-public-message-visible', payload);
|
||||
break;
|
||||
case 'lower-text':
|
||||
socket.emit('set-lower-message-text', payload);
|
||||
break;
|
||||
case 'toggle-lower-visible':
|
||||
socket.emit('set-lower-message-visible', payload);
|
||||
break;
|
||||
case 'toggle-onAir':
|
||||
socket.emit('set-onAir', payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
const { data, setMessage } = useMessageControlProvider();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -101,37 +20,37 @@ export default function MessageControl() {
|
||||
<InputRow
|
||||
label='Timer screen message'
|
||||
placeholder='only the presenter screens see this'
|
||||
text={pres.text}
|
||||
visible={pres.visible}
|
||||
changeHandler={(event) => messageControl('pres-text', event)}
|
||||
actionHandler={() => messageControl('toggle-pres-visible', !pres.visible)}
|
||||
text={data.presenter.text}
|
||||
visible={data.presenter.visible}
|
||||
changeHandler={(newValue) => setMessage.presenterText(newValue)}
|
||||
actionHandler={() => setMessage.presenterVisible(!data.presenter.visible)}
|
||||
/>
|
||||
<InputRow
|
||||
label='Public screen message'
|
||||
placeholder='public screens will render this'
|
||||
text={publ.text}
|
||||
visible={publ.visible}
|
||||
changeHandler={(event) => messageControl('publ-text', event)}
|
||||
actionHandler={() => messageControl('toggle-publ-visible', !publ.visible)}
|
||||
text={data.public.text}
|
||||
visible={data.public.visible}
|
||||
changeHandler={(newValue) => setMessage.publicText(newValue)}
|
||||
actionHandler={() => setMessage.publicVisible(!data.public.visible)}
|
||||
/>
|
||||
<InputRow
|
||||
label='Lower third message'
|
||||
placeholder='visible in lower third screen'
|
||||
text={lower.text}
|
||||
visible={lower.visible}
|
||||
changeHandler={(event) => messageControl('lower-text', event)}
|
||||
actionHandler={() => messageControl('toggle-lower-visible', !lower.visible)}
|
||||
text={data.lower.text}
|
||||
visible={data.lower.visible}
|
||||
changeHandler={(newValue) => setMessage.lowerText(newValue)}
|
||||
actionHandler={() => setMessage.lowerVisible(!data.lower.visible)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.onAirToggle}>
|
||||
<Tooltip label={onAir ? 'Go Off Air' : 'Go On Air'} openDelay={tooltipDelayMid}>
|
||||
<Tooltip label={data.onAir ? 'Go Off Air' : 'Go On Air'} openDelay={tooltipDelayMid}>
|
||||
<IconButton
|
||||
className={style.btn}
|
||||
size='md'
|
||||
icon={onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
icon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={onAir ? 'solid' : 'outline'}
|
||||
onClick={() => messageControl('toggle-onAir', !onAir)}
|
||||
variant={data.onAir ? 'solid' : 'outline'}
|
||||
onClick={() => setMessage.onAir(!data.onAir)}
|
||||
aria-label='Toggle On Air'
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
border-radius: 4px;
|
||||
background-color: $input-bg;
|
||||
border: $input-border;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.padleft {
|
||||
@@ -42,11 +43,12 @@
|
||||
|
||||
.inputRowActive {
|
||||
.label {
|
||||
color: $light-bg;
|
||||
color: $action-blue;
|
||||
}
|
||||
|
||||
.inline {
|
||||
background-color: $light-bg-transparent;
|
||||
border-color: $action-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,18 @@ export default function Playback(props) {
|
||||
<div className={style.playbackContainer}>
|
||||
<StartIconBtn
|
||||
active={playback === 'start'}
|
||||
clickhandler={() => playbackControl('start')}
|
||||
clickhandler={() => playbackControl.start()}
|
||||
disabled={!selectedId || isRolling || noEvents}
|
||||
/>
|
||||
<PauseIconBtn
|
||||
active={playback === 'pause'}
|
||||
clickhandler={() => playbackControl('pause')}
|
||||
clickhandler={() => playbackControl.pause()}
|
||||
disabled={!selectedId || isRolling || noEvents || playback !== 'start'}
|
||||
/>
|
||||
<RollIconBtn
|
||||
active={playback === 'roll'}
|
||||
disabled={playback === 'roll' || noEvents}
|
||||
clickhandler={() => playbackControl('roll')}
|
||||
clickhandler={() => playbackControl.roll()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -35,6 +35,10 @@ export default function Playback(props) {
|
||||
Playback.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
playbackControl: PropTypes.shape({
|
||||
start: PropTypes.func,
|
||||
pause: PropTypes.func,
|
||||
roll: PropTypes.func,
|
||||
}).isRequired,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -37,6 +37,6 @@ export default memo(PlaybackButtons, areEqual);
|
||||
PlaybackButtons.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
playbackControl: PropTypes.object,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
import React from 'react';
|
||||
|
||||
import { usePlaybackControlProvider } from '../../../common/hooks/useSocketProvider';
|
||||
|
||||
import PlaybackButtons from './PlaybackButtons';
|
||||
import PlaybackTimer from './PlaybackTimer';
|
||||
@@ -7,112 +8,20 @@ import PlaybackTimer from './PlaybackTimer';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
export default function PlaybackControl() {
|
||||
const socket = useSocket();
|
||||
const [playback, setPlayback] = useState(null);
|
||||
const [timer, setTimer] = useState({
|
||||
clock: null,
|
||||
running: null,
|
||||
startedAt: null,
|
||||
expectedFinish: null,
|
||||
secondary: null,
|
||||
});
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
|
||||
const resetTimer = () => {
|
||||
setTimer({
|
||||
running: null,
|
||||
startedAt: null,
|
||||
expectedFinish: null,
|
||||
secondary: null,
|
||||
});
|
||||
};
|
||||
|
||||
// handle incoming messages
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
socket.emit('get-timer');
|
||||
socket.emit('get-playstate');
|
||||
socket.emit('get-selected-id');
|
||||
socket.emit('get-numevents');
|
||||
|
||||
// Handle playstate
|
||||
socket.on('playstate', (data) => {
|
||||
setPlayback(data);
|
||||
});
|
||||
|
||||
// Handle timer
|
||||
socket.on('timer', (data) => {
|
||||
setTimer(data);
|
||||
});
|
||||
|
||||
// Handle selected event
|
||||
socket.on('selected-id', (data) => {
|
||||
setSelectedId(data);
|
||||
});
|
||||
|
||||
socket.on('numevents', (data) => {
|
||||
setNumEvents(data);
|
||||
});
|
||||
|
||||
// Clear listener
|
||||
return () => {
|
||||
socket.off('playstate');
|
||||
socket.off('timer');
|
||||
socket.off('selected-id');
|
||||
socket.off('numevents');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const playbackControl = useCallback(
|
||||
(action) => {
|
||||
switch (action) {
|
||||
case 'start':
|
||||
socket.emit('set-playstate', 'start');
|
||||
break;
|
||||
case 'pause':
|
||||
socket.emit('set-playstate', 'pause');
|
||||
break;
|
||||
case 'roll':
|
||||
socket.emit('set-playstate', 'roll');
|
||||
break;
|
||||
case 'previous':
|
||||
socket.emit('set-playstate', 'previous');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'next':
|
||||
socket.emit('set-playstate', 'next');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'unload':
|
||||
socket.emit('set-playstate', 'unload');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'reload':
|
||||
socket.emit('set-playstate', 'reload');
|
||||
resetTimer();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
const { data, setPlayback } = usePlaybackControlProvider();
|
||||
|
||||
return (
|
||||
<div className={style.mainContainer}>
|
||||
<PlaybackTimer
|
||||
timer={timer}
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
handleIncrement={(amount) => socket.emit('increment-timer', amount)}
|
||||
playback={data.playback}
|
||||
selectedId={data.selectedEventId}
|
||||
handleIncrement={(amount) => setPlayback.delay(amount)}
|
||||
/>
|
||||
<PlaybackButtons
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={numEvents < 1}
|
||||
playbackControl={playbackControl}
|
||||
playback={data.playback}
|
||||
selectedId={data.selectedEventId}
|
||||
noEvents={data.numEvents < 1}
|
||||
playbackControl={setPlayback}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Tooltip } from '@chakra-ui/react';
|
||||
import TimerDisplay from 'common/components/countdown/TimerDisplay';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useTimerProvider } from '../../../common/hooks/useSocketProvider';
|
||||
import { millisToSeconds } from '../../../common/utils/dateConfig';
|
||||
import { stringFromMillis } from '../../../common/utils/time';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
@@ -11,12 +13,7 @@ import style from './PlaybackControl.module.scss';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.timer.running === nextProps.timer.running &&
|
||||
prevProps.timer.isNegative === nextProps.timer.isNegative &&
|
||||
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
|
||||
prevProps.timer.startedAt === nextProps.timer.startedAt &&
|
||||
prevProps.playback === nextProps.playback &&
|
||||
prevProps.timer.secondary === nextProps.timer.secondary &&
|
||||
prevProps.selectedId === nextProps.selectedId
|
||||
);
|
||||
};
|
||||
@@ -29,12 +26,14 @@ const incrementProps = {
|
||||
};
|
||||
|
||||
const PlaybackTimer = (props) => {
|
||||
const { timer, playback, handleIncrement, selectedId } = props;
|
||||
const started = stringFromMillis(timer.startedAt, true);
|
||||
const finish = stringFromMillis(timer.expectedFinish, true);
|
||||
const { playback, handleIncrement, selectedId } = props;
|
||||
const timerData = useTimerProvider();
|
||||
const started = stringFromMillis(timerData.startedAt, true);
|
||||
const finish = stringFromMillis(timerData.expectedFinish, true);
|
||||
const isRolling = playback === 'roll';
|
||||
const isWaiting = timer.secondary > 0 && timer.running == null;
|
||||
const isWaiting = timerData.secondaryTimer > 0 && timerData.current == null;
|
||||
const disableButtons = selectedId == null || isRolling;
|
||||
const isOvertime = timerData.current < 0;
|
||||
|
||||
return (
|
||||
<div className={style.timeContainer}>
|
||||
@@ -42,13 +41,13 @@ const PlaybackTimer = (props) => {
|
||||
<Tooltip label='Roll mode active'>
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||
</Tooltip>
|
||||
<div className={timer.isNegative ? style.indNegativeActive : style.indNegative} />
|
||||
<div className={isOvertime ? style.indNegativeActive : style.indNegative} />
|
||||
<div className={style.indDelay} />
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<TimerDisplay
|
||||
time={isWaiting ? timer.secondary : timer.running}
|
||||
isNegative={timer.isNegative}
|
||||
time={isWaiting ? millisToSeconds(timerData.secondaryTimer) : millisToSeconds(timerData.current)}
|
||||
isNegative={isOvertime}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
@@ -118,7 +117,6 @@ const PlaybackTimer = (props) => {
|
||||
export default memo(PlaybackTimer, areEqual);
|
||||
|
||||
PlaybackTimer.propTypes = {
|
||||
timer: PropTypes.object.isRequired,
|
||||
playback: PropTypes.string,
|
||||
handleIncrement: PropTypes.func.isRequired,
|
||||
selectedId: PropTypes.string,
|
||||
|
||||
@@ -16,25 +16,25 @@ export default function Transport(props) {
|
||||
return (
|
||||
<div className={style.playbackContainer}>
|
||||
<TransportIconBtn
|
||||
clickHandler={() => playbackControl('previous')}
|
||||
clickHandler={() => playbackControl.previous()}
|
||||
disabled={isRolling || noEvents}
|
||||
tooltip='Previous event'
|
||||
icon={<IoPlaySkipBack size='22px' />}
|
||||
/>
|
||||
<TransportIconBtn
|
||||
clickHandler={() => playbackControl('next')}
|
||||
clickHandler={() => playbackControl.next()}
|
||||
disabled={isRolling || noEvents}
|
||||
tooltip='Next event'
|
||||
icon={<IoPlaySkipForward size='22px' />}
|
||||
/>
|
||||
<TransportIconBtn
|
||||
clickHandler={() => playbackControl('reload')}
|
||||
clickHandler={() => playbackControl.reload()}
|
||||
disabled={selectedId == null || isRolling || noEvents}
|
||||
tooltip='Reload event'
|
||||
icon={<IoArrowUndo size='22px' />}
|
||||
/>
|
||||
<UnloadIconBtn
|
||||
clickHandler={() => playbackControl('unload')}
|
||||
clickHandler={() => playbackControl.stop()}
|
||||
disabled={(selectedId == null && !isRolling) || noEvents}
|
||||
/>
|
||||
</div>
|
||||
@@ -44,6 +44,11 @@ export default function Transport(props) {
|
||||
Transport.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
playbackControl: PropTypes.shape({
|
||||
previous: PropTypes.func,
|
||||
next: PropTypes.func,
|
||||
reload: PropTypes.func,
|
||||
stop: PropTypes.func,
|
||||
}).isRequired,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { createRef, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { createRef, useCallback, useContext, useEffect } from 'react';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import Empty from 'common/components/state/Empty';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { showQuickEntryAtom } from '../../../common/atoms/LocalEventSettings';
|
||||
import { CursorContext } from '../../../common/context/CursorContext';
|
||||
import { useEventListProvider } from '../../../common/hooks/useSocketProvider';
|
||||
import EntryBlock from '../EntryBlock/EntryBlock';
|
||||
|
||||
import EventListItem from './EventListItem';
|
||||
@@ -16,11 +16,9 @@ export default function EventList(props) {
|
||||
const { events, eventsHandler } = props;
|
||||
const { cursor, moveCursorUp, moveCursorDown, setCursor, isCursorLocked } =
|
||||
useContext(CursorContext);
|
||||
const socket = useSocket();
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [nextId, setNextId] = useState(null);
|
||||
const cursorRef = createRef();
|
||||
const showQuickEntry = useAtomValue(showQuickEntryAtom);
|
||||
const data = useEventListProvider();
|
||||
|
||||
const insertAtCursor = useCallback(
|
||||
(type, cursor) => {
|
||||
@@ -90,30 +88,6 @@ export default function EventList(props) {
|
||||
};
|
||||
}, [handleKeyPress, cursor, events, setCursor]);
|
||||
|
||||
// handle incoming messages
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// ask for playstate
|
||||
socket.emit('get-selected');
|
||||
socket.emit('get-next-id');
|
||||
|
||||
// Handle playstate
|
||||
socket.on('selected', (data) => {
|
||||
setSelectedId(data.id);
|
||||
});
|
||||
|
||||
socket.on('next-id', (data) => {
|
||||
setNextId(data);
|
||||
});
|
||||
|
||||
// Clear listener
|
||||
return () => {
|
||||
socket.off('selected');
|
||||
socket.off('next-id');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
// when cursor moves, view should follow
|
||||
useEffect(() => {
|
||||
if (cursorRef.current == null) return;
|
||||
@@ -129,14 +103,14 @@ export default function EventList(props) {
|
||||
// or cursor settings changed
|
||||
useEffect(() => {
|
||||
// and if we are locked
|
||||
if (!isCursorLocked || selectedId == null) return;
|
||||
if (!isCursorLocked || data.selectedEventId == null) return;
|
||||
|
||||
// move cursor
|
||||
let gotoIndex = -1;
|
||||
let found = false;
|
||||
for (const e of events) {
|
||||
gotoIndex++;
|
||||
if (e.id === selectedId) {
|
||||
if (e.id === data.selectedEventId) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -146,7 +120,7 @@ export default function EventList(props) {
|
||||
setCursor(gotoIndex);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedId, isCursorLocked]);
|
||||
}, [data.selectedEventId, isCursorLocked]);
|
||||
|
||||
// DND
|
||||
const handleOnDragEnd = useCallback(
|
||||
@@ -210,8 +184,8 @@ export default function EventList(props) {
|
||||
index={index}
|
||||
eventIndex={eventIndex}
|
||||
data={e}
|
||||
selected={selectedId === e.id}
|
||||
next={nextId === e.id}
|
||||
selected={data.selectedEventId === e.id}
|
||||
next={data.nextEventId === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
previousEnd={previousEnd}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { EVENTS_TABLE } from 'common/api/apiConstants';
|
||||
import {
|
||||
fetchAllEvents,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect,useState } from 'react';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
import React from 'react';
|
||||
|
||||
import { useInfoProvider } from '../../common/hooks/useSocketProvider';
|
||||
|
||||
import InfoLogger from './InfoLogger';
|
||||
import InfoNif from './InfoNif';
|
||||
@@ -8,75 +9,28 @@ import InfoTitle from './InfoTitle';
|
||||
import style from './Info.module.scss';
|
||||
|
||||
export default function Info() {
|
||||
const socket = useSocket();
|
||||
const [titles, setTitles] = useState({
|
||||
titleNow: '',
|
||||
subtitleNow: '',
|
||||
presenterNow: '',
|
||||
noteNow: '',
|
||||
titleNext: '',
|
||||
subtitleNext: '',
|
||||
presenterNext: '',
|
||||
noteNext: ''
|
||||
});
|
||||
const [selected, setSelected] = useState('No events');
|
||||
const [playback, setPlayback] = useState(null);
|
||||
const data = useInfoProvider();
|
||||
|
||||
// handle incoming messages
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Ask for titles
|
||||
socket.emit('get-titles');
|
||||
|
||||
// Handle titles
|
||||
socket.on('titles', (data) => {
|
||||
setTitles(data);
|
||||
});
|
||||
|
||||
// Handle playstate
|
||||
socket.on('playstate', (data) => {
|
||||
setPlayback(data);
|
||||
});
|
||||
|
||||
// Ask for selection data
|
||||
socket.emit('get-selected');
|
||||
|
||||
// Handle selection data
|
||||
socket.on('selected', (data) => {
|
||||
if (data.total === 0 || data.total == null) {
|
||||
setSelected('No events');
|
||||
} else {
|
||||
const formattedCurrent = `Event ${
|
||||
data.index != null ? data.index + 1 : '-'
|
||||
}/${data.total ? data.total : '-'}`;
|
||||
setSelected(formattedCurrent);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear listener
|
||||
return () => {
|
||||
socket.off('titles');
|
||||
socket.off('selected');
|
||||
socket.off('playstate');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
// prepare data
|
||||
const titlesNow = {
|
||||
title: titles.titleNow,
|
||||
subtitle: titles.subtitleNow,
|
||||
presenter: titles.presenterNow,
|
||||
note: titles.noteNow
|
||||
title: data.titles.titleNow,
|
||||
subtitle: data.titles.subtitleNow,
|
||||
presenter: data.titles.presenterNow,
|
||||
note: data.titles.noteNow,
|
||||
};
|
||||
|
||||
const titlesNext = {
|
||||
title: titles.titleNext,
|
||||
subtitle: titles.subtitleNext,
|
||||
presenter: titles.presenterNext,
|
||||
note: titles.noteNext
|
||||
title: data.titles.titleNext,
|
||||
subtitle: data.titles.subtitleNext,
|
||||
presenter: data.titles.presenterNext,
|
||||
note: data.titles.noteNext,
|
||||
};
|
||||
|
||||
const selected = !data.numEvents
|
||||
? 'No events'
|
||||
: `Event ${data.selectedEventIndex != null ? data.selectedEventIndex + 1 : '-'}/${
|
||||
data.numEvents ? data.numEvents : '-'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.main}>
|
||||
@@ -84,8 +38,8 @@ export default function Info() {
|
||||
<span>{selected}</span>
|
||||
</div>
|
||||
<InfoNif />
|
||||
<InfoTitle title='Now' data={titlesNow} roll={playback === 'roll'} />
|
||||
<InfoTitle title='Next' data={titlesNext} roll={playback === 'roll'} />
|
||||
<InfoTitle title='Now' data={titlesNow} roll={data.playback === 'roll'} />
|
||||
<InfoTitle title='Next' data={titlesNext} roll={data.playback === 'roll'} />
|
||||
<InfoLogger />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import SocketProvider from 'common/context/socketContext';
|
||||
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
import Info from '../Info';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
test('check static info render', async () => {
|
||||
// need to inject the socket provider to make component
|
||||
// render without failing
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryClientProvider client={queryClientMock}>
|
||||
<SocketProvider>
|
||||
<Info />
|
||||
</SocketProvider>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
|
||||
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
|
||||
@@ -7,6 +6,7 @@ import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
|
||||
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { EVENTS_TABLE } from 'common/api/apiConstants';
|
||||
import { downloadEvents, uploadEvents } from 'common/api/ontimeApi';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
|
||||
@@ -31,7 +31,7 @@ import { defaultColumnOrder, defaultHiddenColumns } from './defaults';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function OntimeTable({ tableData, userFields, handleUpdate, selectedId }) {
|
||||
export default function OntimeTable({ tableData, userFields, selectedId, handleUpdate }) {
|
||||
const { followSelected, showSettings } = useContext(TableSettingsContext);
|
||||
const [columnOrder, saveColumnOrder] = useLocalStorage('table-order', defaultColumnOrder);
|
||||
const [columnSize, saveColumnSize] = useLocalStorage('table-sizes', {});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { Divider } from '@chakra-ui/layout';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
@@ -10,10 +10,10 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { EVENT_TABLE } from '../../common/api/apiConstants';
|
||||
import { fetchEvent } from '../../common/api/eventApi';
|
||||
import { useSocket } from '../../common/context/socketContext';
|
||||
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 { formatDisplay } from '../../common/utils/dateConfig';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||
@@ -22,85 +22,18 @@ import PlaybackIcon from './tableElements/PlaybackIcon';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableHeader({ handleCSVExport }) {
|
||||
export default function TableHeader({handleCSVExport, featureData}) {
|
||||
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } =
|
||||
useContext(TableSettingsContext);
|
||||
const { data } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
const timer = useTimerProvider();
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const { data: event } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
|
||||
const socket = useSocket();
|
||||
const [timer, setTimer] = useState({
|
||||
clock: null,
|
||||
running: null,
|
||||
startedAt: null,
|
||||
expectedFinish: null,
|
||||
secondary: null,
|
||||
});
|
||||
|
||||
const [titles, setTitles] = useState({
|
||||
titleNow: '',
|
||||
subtitleNow: '',
|
||||
presenterNow: '',
|
||||
noteNow: '',
|
||||
titleNext: '',
|
||||
subtitleNext: '',
|
||||
presenterNext: '',
|
||||
noteNext: '',
|
||||
});
|
||||
|
||||
const [playback, setPlayback] = useState('');
|
||||
const [selected, setSelected] = useState('');
|
||||
|
||||
/**
|
||||
* Handle incoming data from socket
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Ask for titles
|
||||
socket.emit('get-titles');
|
||||
|
||||
// Ask for timer
|
||||
socket.emit('get-timer');
|
||||
|
||||
// Ask for playback state
|
||||
socket.emit('get-playstate');
|
||||
|
||||
// Handle titles
|
||||
socket.on('titles', (data) => {
|
||||
setTitles(data);
|
||||
});
|
||||
|
||||
// Handle timer
|
||||
socket.on('timer', (data) => {
|
||||
setTimer(data);
|
||||
});
|
||||
|
||||
// Handle playstate
|
||||
socket.on('playstate', (data) => {
|
||||
setPlayback(data);
|
||||
});
|
||||
|
||||
// Handle selection data
|
||||
socket.on('selected', (data) => {
|
||||
if (data.total === 0 || data.total == null) {
|
||||
setSelected('');
|
||||
} else {
|
||||
const formattedCurrent = `${data.index != null ? data.index + 1 : '-'}/${
|
||||
data.total ? data.total : '-'
|
||||
}`;
|
||||
setSelected(formattedCurrent);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear listener
|
||||
return () => {
|
||||
socket.off('titles');
|
||||
socket.off('timer');
|
||||
socket.off('playstate');
|
||||
socket.off('selected');
|
||||
};
|
||||
}, [socket]);
|
||||
const selected = !featureData.numEvents
|
||||
? 'No events'
|
||||
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
|
||||
featureData.numEvents ? featureData.numEvents : '-'
|
||||
}`;
|
||||
|
||||
// prepare presentation variables
|
||||
const timerNow = `${timer.running < 0 ? '-' : ''}${formatDisplay(timer.running)}`;
|
||||
@@ -111,12 +44,12 @@ export default function TableHeader({ handleCSVExport }) {
|
||||
|
||||
return (
|
||||
<div className={style.header}>
|
||||
<div className={style.headerName}>{data?.title || ''}</div>
|
||||
<div className={style.headerNow}>{titles.titleNow}</div>
|
||||
<div className={style.headerName}>{event?.title || ''}</div>
|
||||
<div className={style.headerNow}>{featureData.titleNow}</div>
|
||||
<div className={style.headerPlayback}>
|
||||
<span className={style.label}>{selected}</span>
|
||||
<br />
|
||||
<PlaybackIcon state={playback} />
|
||||
<PlaybackIcon state={featureData.playback} />
|
||||
</div>
|
||||
<div className={style.headerRunning}>
|
||||
<span className={style.label}>Running Timer</span>
|
||||
@@ -155,7 +88,7 @@ export default function TableHeader({ handleCSVExport }) {
|
||||
</Tooltip>
|
||||
<Divider />
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Export to CSV'>
|
||||
<span className={style.actionText} onClick={() => handleCSVExport(data)}>
|
||||
<span className={style.actionText} onClick={() => handleCSVExport(event)}>
|
||||
CSV
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -166,4 +99,11 @@ export default function TableHeader({ handleCSVExport }) {
|
||||
|
||||
TableHeader.propTypes = {
|
||||
handleCSVExport: PropTypes.func.isRequired,
|
||||
featureData: PropTypes.shape({
|
||||
playback: PropTypes.string,
|
||||
selectedEventId: PropTypes.string,
|
||||
selectedEventIndex: PropTypes.string,
|
||||
numEvents: PropTypes.number,
|
||||
titleNow: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { 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 { useSocket } from '../../common/context/socketContext';
|
||||
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 OntimeTable from './OntimeTable';
|
||||
import TableHeader from './TableHeader';
|
||||
@@ -15,35 +15,16 @@ import { makeCSV, makeTable } from './utils';
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableWrapper() {
|
||||
const { data: tableData } = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const { data: events } = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const { data: userFields } = useFetch(USERFIELDS, getUserFields);
|
||||
const mutation = useMutateEvents(requestPatch);
|
||||
const socket = useSocket();
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const { theme } = useContext(TableSettingsContext);
|
||||
const featureData = useCuesheetProvider();
|
||||
|
||||
// Set window title
|
||||
document.title = 'ontime - Cuesheet';
|
||||
|
||||
/**
|
||||
* Handle incoming data from socket
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Ask for selected
|
||||
socket.emit('get-selected');
|
||||
|
||||
// Handle selected
|
||||
socket.on('selected', (data) => {
|
||||
setSelectedId(data.id);
|
||||
});
|
||||
|
||||
// Clear listener
|
||||
return () => {
|
||||
socket.off('selected');
|
||||
};
|
||||
}, [socket]);
|
||||
document.title = 'ontime - Cuesheet';
|
||||
}, []);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
async (rowIndex, accessor, payload) => {
|
||||
@@ -51,11 +32,11 @@ export default function TableWrapper() {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if value is the same
|
||||
const event = tableData[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
// check if value is the same
|
||||
const event = events[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event[accessor] === payload) {
|
||||
return;
|
||||
@@ -73,23 +54,21 @@ export default function TableWrapper() {
|
||||
[accessor]: cleanVal,
|
||||
};
|
||||
|
||||
// submit
|
||||
try {
|
||||
await mutation.mutateAsync(mutationObject);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
[mutation, tableData]
|
||||
);
|
||||
// submit
|
||||
try {
|
||||
await mutation.mutateAsync(mutationObject);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [mutation, events]);
|
||||
|
||||
const exportHandler = useCallback(
|
||||
(headerData) => {
|
||||
if (!headerData || !tableData || !userFields) {
|
||||
if (!headerData || !events || !userFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetData = makeTable(headerData, tableData, userFields);
|
||||
const sheetData = makeTable(headerData, events, userFields);
|
||||
const csvContent = makeCSV(sheetData);
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement('a');
|
||||
@@ -98,20 +77,20 @@ export default function TableWrapper() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
},
|
||||
[tableData, userFields]
|
||||
[events, userFields]
|
||||
);
|
||||
|
||||
if (typeof tableData === 'undefined' || typeof userFields === 'undefined') {
|
||||
if (typeof events === 'undefined' || typeof userFields === 'undefined') {
|
||||
return <span>loading...</span>;
|
||||
}
|
||||
return (
|
||||
<div className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}>
|
||||
<TableHeader handleCSVExport={exportHandler} />
|
||||
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
|
||||
<OntimeTable
|
||||
tableData={tableData}
|
||||
tableData={events}
|
||||
userFields={userFields}
|
||||
handleUpdate={handleUpdate}
|
||||
selectedId={selectedId}
|
||||
selectedId={featureData.selectedEventId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user