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:
Carlos Valente
2022-09-06 21:11:24 +02:00
committed by GitHub
parent 0e450cb6cb
commit b384284175
35 changed files with 667 additions and 630 deletions
@@ -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,
};
+8 -34
View File
@@ -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,
+20 -66
View File
@@ -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 -1
View File
@@ -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';
+1 -1
View File
@@ -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', {});
+21 -81
View File
@@ -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,
}),
};
+25 -46
View File
@@ -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>
);