v2 lazy rundown (#317)

* chore: update related deps

* refactor: memoise entrypoints

* refactor: store selectors

* refactor: improve component performance

* chore: remove unused dependencies

* refactor: improve component dnd
This commit is contained in:
Carlos Valente
2023-03-30 21:22:36 +02:00
committed by GitHub
parent 03d5389540
commit 9338a615d4
27 changed files with 701 additions and 698 deletions
+2 -4
View File
@@ -4,8 +4,8 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.5.1", "@chakra-ui/react": "^2.5.1",
"@dnd-kit/core": "^6.0.6", "@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.1", "@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1", "@dnd-kit/utilities": "^3.2.1",
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
@@ -22,7 +22,6 @@
"framer-motion": "^8.0.2", "framer-motion": "^8.0.2",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-hook-form": "^7.43.5", "react-hook-form": "^7.43.5",
@@ -67,7 +66,6 @@
"@types/luxon": "^3.2.0", "@types/luxon": "^3.2.0",
"@types/prop-types": "^15.7.5", "@types/prop-types": "^15.7.5",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-beautiful-dnd": "^13.1.3",
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
"@types/testing-library__jest-dom": "^5.14.5", "@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.48.1",
@@ -1,85 +0,0 @@
import { createContext, ReactNode, useCallback, useMemo, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
interface CursorContextState {
cursor: number;
isCursorLocked: boolean;
toggleCursorLocked: (newValue?: boolean) => void;
setCursor: (index: number) => void;
moveCursorUp: () => void;
moveCursorDown: () => void;
moveCursorTo: (index: number) => void;
}
export const CursorContext = createContext<CursorContextState>({
cursor: 0,
isCursorLocked: false,
toggleCursorLocked: () => undefined,
setCursor: () => undefined,
moveCursorUp: () => undefined,
moveCursorDown: () => undefined,
moveCursorTo: () => undefined,
});
interface CursorProviderProps {
children: ReactNode
}
export const CursorProvider = ({ children }: CursorProviderProps) => {
const [cursor, setCursor] = useState(0);
const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked');
const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]);
const cursorLockedOff = useCallback(() => _setCursorLocked('unlocked'), [_setCursorLocked]);
const cursorLockedOn = useCallback(() => _setCursorLocked('locked'), [_setCursorLocked]);
const moveCursorUp = useCallback(() => {
setCursor((prev) => Math.max(prev - 1, 0));
}, []);
const moveCursorDown = useCallback(() => {
setCursor((prev) => prev + 1);
}, []);
/**
* @param {boolean | undefined} newValue
*/
const toggleCursorLocked = useCallback(
(newValue?: boolean) => {
if (typeof newValue === 'undefined') {
if (isCursorLocked) {
cursorLockedOff();
} else {
cursorLockedOn();
}
} else if (!newValue) {
cursorLockedOff();
} else if (newValue) {
cursorLockedOn();
}
},
[cursorLockedOff, cursorLockedOn, isCursorLocked]
);
// moves cursor to given index
const moveCursorTo = useCallback((index: number) => {
setCursor(index);
}, []);
return (
<CursorContext.Provider
value={{
cursor,
isCursorLocked,
toggleCursorLocked,
setCursor,
moveCursorUp,
moveCursorDown,
moveCursorTo,
}}
>
{children}
</CursorContext.Provider>
);
};
@@ -22,7 +22,7 @@ import { useEmitLog } from '../stores/logger';
export const useEventAction = () => { export const useEventAction = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { emitError } = useEmitLog(); const { emitError } = useEmitLog();
const { eventSettings } = useLocalEvent(); const eventSettings = useLocalEvent((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
@@ -0,0 +1,24 @@
import { create } from 'zustand';
import { booleanFromLocalStorage } from '../utils/localStorage';
type CursorStore = {
cursor: number;
isCursorLocked: boolean;
toggleCursorLocked: (newValue?: boolean) => void;
moveCursorTo: (index: number) => void;
};
const cursorLockedKey = 'ontime-cursor-islocked';
export const useCursor = create<CursorStore>()((set) => ({
cursor: 0,
isCursorLocked: booleanFromLocalStorage(cursorLockedKey, false),
toggleCursorLocked: (newValue?: boolean) =>
set((state) => {
const val = typeof newValue === 'undefined' ? !state.isCursorLocked : newValue;
localStorage.setItem(cursorLockedKey, String(val));
return { isCursorLocked: val };
}),
moveCursorTo: (index: number) => set(() => ({ cursor: index })),
}));
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
@@ -8,9 +9,9 @@ import MessageControl from './MessageControl';
import style from '../../editors/Editor.module.scss'; import style from '../../editors/Editor.module.scss';
export default function MessageControlExport() { const MessageControlExport = () => {
return ( return (
<Box className={style.messages} data-testid="panel-messages-control"> <Box className={style.messages} data-testid='panel-messages-control'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} /> <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
<div className={style.content}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
@@ -19,4 +20,6 @@ export default function MessageControlExport() {
</div> </div>
</Box> </Box>
); );
} };
export default memo(MessageControlExport);
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
@@ -8,7 +9,7 @@ import PlaybackControl from './PlaybackControl';
import style from '../../editors/Editor.module.scss'; import style from '../../editors/Editor.module.scss';
export default function TimerControlExport() { const TimerControlExport = () => {
return ( return (
<Box className={style.playback} data-testid='panel-timer-control'> <Box className={style.playback} data-testid='panel-timer-control'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} /> <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
@@ -19,4 +20,6 @@ export default function TimerControlExport() {
</div> </div>
</Box> </Box>
); );
} };
export default memo(TimerControlExport);
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { Box, IconButton } from '@chakra-ui/react'; import { Box, IconButton } from '@chakra-ui/react';
import { FiX } from '@react-icons/all-files/fi/FiX'; import { FiX } from '@react-icons/all-files/fi/FiX';
@@ -16,7 +17,7 @@ const closeBtnStyle = {
_hover: { bg: '#ebedf0', color: '#333' }, _hover: { bg: '#ebedf0', color: '#333' },
}; };
export default function InfoExport() { const EventEditorExport = () => {
const { openId, removeOpenEvent } = useEventEditorStore(); const { openId, removeOpenEvent } = useEventEditorStore();
return ( return (
@@ -31,4 +32,7 @@ export default function InfoExport() {
</ErrorBoundary> </ErrorBoundary>
</Box> </Box>
); );
} };
export default memo(EventEditorExport);
+5 -2
View File
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
@@ -8,9 +9,9 @@ import Info from './Info';
import style from '../editors/Editor.module.scss'; import style from '../editors/Editor.module.scss';
export default function InfoExport() { const InfoExport = () => {
return ( return (
<Box className={style.info} data-testid="panel-info"> <Box className={style.info} data-testid='panel-info'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'info')} /> <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'info')} />
<div className={style.content}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
@@ -20,3 +21,5 @@ export default function InfoExport() {
</Box> </Box>
); );
} }
export default memo(InfoExport)
+26 -35
View File
@@ -1,4 +1,4 @@
import { memo, useCallback, useContext } from 'react'; import { memo, useCallback } from 'react';
import { Button, HStack, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Switch } from '@chakra-ui/react'; import { Button, HStack, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Switch } from '@chakra-ui/react';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle'; import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2'; import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
@@ -6,36 +6,32 @@ import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline'; import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { SupportedEvent } from 'ontime-types'; import { SupportedEvent } from 'ontime-types';
import { CursorContext } from '../../common/context/CursorContext';
import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventAction } from '../../common/hooks/useEventAction';
import { useCursor } from '../../common/stores/cursorStore';
import style from './RundownMenu.module.scss'; import style from './RundownMenu.module.scss';
const RundownMenu = () => { const RundownMenu = () => {
const { isCursorLocked, toggleCursorLocked } = useContext(CursorContext); const isCursorLocked = useCursor((state) => state.isCursorLocked);
const toggleCursorLocked = useCursor((state) => state.toggleCursorLocked);
const { addEvent, deleteAllEvents } = useEventAction(); const { addEvent, deleteAllEvents } = useEventAction();
// TODO: re-write this with stable functions const newEvent = useCallback(() => {
type ActionTypes = SupportedEvent | 'delete-all'; addEvent({ type: SupportedEvent.Event });
const eventAction = useCallback( }, [addEvent]);
(action: ActionTypes) => {
switch (action) { const newBlock = useCallback(() => {
case SupportedEvent.Event: addEvent({ type: SupportedEvent.Block });
addEvent({ type: action }); }, [addEvent]);
break;
case SupportedEvent.Delay: const newDelay = useCallback(() => {
addEvent({ type: action }); addEvent({ type: SupportedEvent.Delay });
break; }, [addEvent]);
case SupportedEvent.Block:
addEvent({ type: action }); const deleteAll = useCallback(() => {
break; deleteAllEvents();
case 'delete-all': }, [deleteAllEvents]);
deleteAllEvents();
break;
}
},
[addEvent, deleteAllEvents],
);
return ( return (
<HStack className={style.headerButtons}> <HStack className={style.headerButtons}>
@@ -45,29 +41,24 @@ const RundownMenu = () => {
onChange={(event) => toggleCursorLocked(event.target.checked)} onChange={(event) => toggleCursorLocked(event.target.checked)}
variant='ontime' variant='ontime'
/> />
Lock cursor to current Follow loaded event
</label> </label>
<Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'> <Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<MenuButton <MenuButton as={Button} leftIcon={<IoAdd />} size='sm' variant='ontime-subtle'>
as={Button}
leftIcon={<IoAdd />}
size='sm'
variant='ontime-subtle'
>
Event... Event...
</MenuButton> </MenuButton>
<MenuList> <MenuList>
<MenuItem icon={<IoAdd />} onClick={() => eventAction(SupportedEvent.Event)}> <MenuItem icon={<IoAdd />} onClick={newEvent}>
Add event at start Add event at start
</MenuItem> </MenuItem>
<MenuItem icon={<IoTimerOutline />} onClick={() => eventAction(SupportedEvent.Delay)}> <MenuItem icon={<IoTimerOutline />} onClick={newDelay}>
Add delay at start Add delay at start
</MenuItem> </MenuItem>
<MenuItem icon={<FiMinusCircle />} onClick={() => eventAction(SupportedEvent.Block)}> <MenuItem icon={<FiMinusCircle />} onClick={newBlock}>
Add block at start Add block at start
</MenuItem> </MenuItem>
<MenuDivider /> <MenuDivider />
<MenuItem icon={<FiTrash2 />} onClick={() => eventAction('delete-all')} color='#D20300'> <MenuItem icon={<FiTrash2 />} onClick={deleteAll} color='#D20300'>
Delete all events Delete all events
</MenuItem> </MenuItem>
</MenuList> </MenuList>
@@ -37,7 +37,9 @@ export default function AppSettingsModal() {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [hidePin, setHidePin] = useState(true); const [hidePin, setHidePin] = useState(true);
const { eventSettings, setLocalEventSettings } = useLocalEvent(); const eventSettings = useLocalEvent((state) => state.eventSettings);
const setLocalEventSettings = useLocalEvent((state) => state.setLocalEventSettings);
const [formSettings, setFormSettings] = useState(eventSettings); const [formSettings, setFormSettings] = useState(eventSettings);
const [updateMessage, setUpdateMessage] = useState(<a>Using ontime version: {version}</a>); const [updateMessage, setUpdateMessage] = useState(<a>Using ontime version: {version}</a>);
@@ -9,6 +9,7 @@
} }
.list { .list {
overflow-x: clip;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
+110 -108
View File
@@ -1,17 +1,16 @@
import { createRef, Fragment, useCallback, useContext, useEffect } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { Button } from '@chakra-ui/react'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { OntimeRundown, SupportedEvent } from 'ontime-types'; import { OntimeRundown, SupportedEvent } from 'ontime-types';
import Empty from '../../common/components/state/Empty';
import { CursorContext } from '../../common/context/CursorContext';
import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventAction } from '../../common/hooks/useEventAction';
import { useRundownEditor } from '../../common/hooks/useSocket'; import { useRundownEditor } from '../../common/hooks/useSocket';
import { useCursor } from '../../common/stores/cursorStore';
import { useLocalEvent } from '../../common/stores/localEvent'; import { useLocalEvent } from '../../common/stores/localEvent';
import { cloneEvent } from '../../common/utils/eventsManager'; import { cloneEvent } from '../../common/utils/eventsManager';
import QuickAddBlock from './quick-add-block/QuickAddBlock'; import QuickAddBlock from './quick-add-block/QuickAddBlock';
import RundownEmpty from './RundownEmpty';
import RundownEntry from './RundownEntry'; import RundownEntry from './RundownEntry';
import style from './Rundown.module.scss'; import style from './Rundown.module.scss';
@@ -22,16 +21,24 @@ interface RundownProps {
export default function Rundown(props: RundownProps) { export default function Rundown(props: RundownProps) {
const { entries } = props; const { entries } = props;
const data = useRundownEditor(); const [statefulEntries, setStatefulEntries] = useState(entries);
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
const { addEvent, reorderEvent } = useEventAction();
const cursorRef = createRef<HTMLDivElement>();
const { eventSettings } = useLocalEvent(); const featureData = useRundownEditor();
const { addEvent, reorderEvent } = useEventAction();
const eventSettings = useLocalEvent((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
const showQuickEntry = eventSettings.showQuickEntry; const showQuickEntry = eventSettings.showQuickEntry;
// cursor
const cursor = useCursor((state) => state.cursor);
const isCursorLocked = useCursor((state) => state.isCursorLocked);
const moveCursorTo = useCursor((state) => state.moveCursorTo);
const cursorRef = useRef<HTMLDivElement>();
// DND KIT
const sensors = useSensors(useSensor(PointerSensor));
const insertAtCursor = useCallback( const insertAtCursor = useCallback(
(type: SupportedEvent | 'clone', cursor: number) => { (type: SupportedEvent | 'clone', cursor: number) => {
if (cursor === -1) { if (cursor === -1) {
@@ -78,11 +85,11 @@ export default function Rundown(props: RundownProps) {
if (event.altKey && (!event.ctrlKey || !event.shiftKey)) { if (event.altKey && (!event.ctrlKey || !event.shiftKey)) {
switch (event.code) { switch (event.code) {
case 'ArrowDown': { case 'ArrowDown': {
if (cursor < entries.length - 1) moveCursorDown(); if (cursor < entries.length - 1) moveCursorTo(cursor + 1);
break; break;
} }
case 'ArrowUp': { case 'ArrowUp': {
if (cursor > 0) moveCursorUp(); if (cursor > 0) moveCursorTo(cursor - 1);
break; break;
} }
case 'KeyE': { case 'KeyE': {
@@ -112,25 +119,35 @@ export default function Rundown(props: RundownProps) {
} }
} }
}, },
[cursor, entries.length, insertAtCursor, moveCursorDown, moveCursorUp], [cursor, entries.length, insertAtCursor, moveCursorTo],
); );
// we copy the state from the store here
// to workaround async updates on the drag mutations
useEffect(() => {
if (entries) {
setStatefulEntries(entries);
}
}, [entries]);
// listen to keys
useEffect(() => { useEffect(() => {
// attach the event listener
document.addEventListener('keydown', handleKeyPress); document.addEventListener('keydown', handleKeyPress);
if (cursor > entries.length - 1) moveCursorTo(entries.length - 1);
if (entries.length > 0 && cursor === -1) moveCursorTo(0);
// remove the event listener
return () => { return () => {
document.removeEventListener('keydown', handleKeyPress); document.removeEventListener('keydown', handleKeyPress);
}; };
}, [handleKeyPress, cursor, entries, moveCursorTo]); }, [handleKeyPress]);
// when cursor moves, view should follow // when cursor moves, view should follow
useEffect(() => { useEffect(() => {
if (cursorRef.current == null) return; if (!cursorRef?.current) return;
// using start in block parameter causes jumpy behaviour
// could alternatively scroll using scrollTo and
// calculate position within a range
// if the item is near the top half, we are ok
// otherwise scroll difference
cursorRef.current.scrollIntoView({ cursorRef.current.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'nearest', block: 'nearest',
@@ -142,120 +159,105 @@ export default function Rundown(props: RundownProps) {
// or cursor settings changed // or cursor settings changed
useEffect(() => { useEffect(() => {
// and if we are locked // and if we are locked
if (!isCursorLocked || !data?.selectedEventId) { if (!isCursorLocked || !featureData?.selectedEventId) {
return; return;
} }
// move cursor // move cursor
let gotoIndex = -1; let gotoIndex = -1;
let found = false; let found = false;
for (const e of entries) { for (const entry of entries) {
gotoIndex++; gotoIndex++;
if (e.id === data.selectedEventId) { if (entry.id === featureData.selectedEventId) {
found = true; found = true;
break; break;
} }
} }
if (found) { if (found) {
// move cursor
moveCursorTo(gotoIndex); moveCursorTo(gotoIndex);
} }
}, [data?.selectedEventId, entries, isCursorLocked, moveCursorTo]); }, [featureData?.selectedEventId, entries, isCursorLocked, moveCursorTo]);
const handleOnDragEnd = useCallback( const handleOnDragEnd = (event: DragEndEvent) => {
(result: DropResult) => { const { active, over } = event;
// drop outside of area
if (!result?.destination) return;
// no change if (over?.id) {
if (result.destination.index === result.source.index) return; if (active.id !== over?.id) {
const fromIndex = active.data.current?.sortable.index;
// Call API const toIndex = over.data.current?.sortable.index;
reorderEvent(result.draggableId, result.source.index, result.destination.index); // ugly hack to handle inconsistencies between dnd-kit and async store updates
}, setStatefulEntries((currentEntries) => {
[reorderEvent], return arrayMove(currentEntries, fromIndex, toIndex);
); });
reorderEvent(String(active.id), fromIndex, toIndex);
}
}
};
if (!entries.length) { if (!entries.length) {
return ( return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, -1)} />;
<div className={style.alignCenter}>
<Empty text='No data yet' style={{ marginTop: '7vh' }} />
<Button
onClick={() => insertAtCursor(SupportedEvent.Event, -1)}
variant='ontime-filled'
className={style.spaceTop}
leftIcon={<IoAdd />}
>
Create Event
</Button>
</div>
);
} }
let cumulativeDelay = 0; let cumulativeDelay = 0;
let eventIndex = -1;
let previousEnd = 0; let previousEnd = 0;
let thisEnd = 0; let thisEnd = 0;
let previousEventId: string | undefined; let previousEventId: string | undefined;
let eventIndex = -1;
return ( return (
<div className={style.eventContainer}> <div className={style.eventContainer}>
<DragDropContext onDragEnd={handleOnDragEnd}> <DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
<Droppable droppableId='eventlist'> <SortableContext items={statefulEntries} strategy={verticalListSortingStrategy}>
{(provided) => ( <div className={style.list}>
<div className={style.list} {...provided.droppableProps} ref={provided.innerRef}> {statefulEntries.map((entry, index) => {
{entries.map((entry, index) => { if (index === 0) {
if (index === 0) { cumulativeDelay = 0;
cumulativeDelay = 0; eventIndex = -1;
eventIndex = -1; }
} if (entry.type === SupportedEvent.Delay && entry.duration !== null) {
if (entry.type === 'delay' && entry.duration != null) { cumulativeDelay += entry.duration;
cumulativeDelay += entry.duration; } else if (entry.type === SupportedEvent.Block) {
} else if (entry.type === 'block') { cumulativeDelay = 0;
cumulativeDelay = 0; } else if (entry.type === SupportedEvent.Event) {
} else if (entry.type === 'event') { eventIndex++;
eventIndex++; previousEnd = thisEnd;
previousEnd = thisEnd; thisEnd = entry.timeEnd;
thisEnd = entry.timeEnd; previousEventId = entry.id;
previousEventId = entry.id; }
} const isLast = index === entries.length - 1;
const isLast = index === entries.length - 1; const isSelected = featureData?.selectedEventId === entry.id;
const isSelected = data?.selectedEventId === entry.id; const isNext = featureData?.nextEventId === entry.id;
const isNext = data?.nextEventId === entry.id;
return ( return (
<Fragment key={entry.id}> <div key={entry.id} ref={cursor === index ? cursorRef : undefined}>
<div ref={cursor === index ? cursorRef : undefined}> <RundownEntry
<RundownEntry type={entry.type}
type={entry.type} index={index}
index={index} eventIndex={eventIndex}
eventIndex={eventIndex} data={entry}
data={entry} selected={isSelected}
selected={isSelected} hasCursor={cursor === index}
hasCursor={cursor === index} next={isNext}
next={isNext} delay={cumulativeDelay}
delay={cumulativeDelay} previousEnd={previousEnd}
previousEnd={previousEnd} previousEventId={previousEventId}
previousEventId={previousEventId} playback={isSelected ? featureData.playback : undefined}
playback={isSelected ? data.playback || undefined : undefined} />
/> {((showQuickEntry && index === cursor) || isLast) && (
</div> <QuickAddBlock
{((showQuickEntry && index === cursor) || isLast) && ( showKbd={false}
<QuickAddBlock eventId={entry.id}
showKbd={index === cursor} previousEventId={previousEventId}
eventId={entry.id} disableAddDelay={entry.type === 'delay'}
previousEventId={previousEventId} disableAddBlock={entry.type === 'block'}
disableAddDelay={entry.type === 'delay'} />
disableAddBlock={entry.type === 'block'} )}
/> </div>
)} );
</Fragment> })}
); </div>
})} </SortableContext>
{provided.placeholder} </DndContext>
</div>
)}
</Droppable>
</DragDropContext>
</div> </div>
); );
} }
@@ -0,0 +1,23 @@
import { Button } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import Empty from '../../common/components/state/Empty';
import style from './Rundown.module.scss';
interface RundownEmptyProps {
handleAddNew: () => void;
}
export default function RundownEmpty(props: RundownEmptyProps) {
const { handleAddNew } = props;
return (
<div className={style.alignCenter}>
<Empty text='No data yet' style={{ marginTop: '7vh' }} />
<Button onClick={handleAddNew} variant='ontime-filled' className={style.spaceTop} leftIcon={<IoAdd />}>
Create Event
</Button>
</div>
);
}
@@ -1,7 +1,6 @@
import { useCallback, useContext } from 'react'; import { useCallback } from 'react';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
import { CursorContext } from '../../common/context/CursorContext';
import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventAction } from '../../common/hooks/useEventAction';
import { useEventEditorStore } from '../../common/stores/eventEditor'; import { useEventEditorStore } from '../../common/stores/eventEditor';
import { useLocalEvent } from '../../common/stores/localEvent'; import { useLocalEvent } from '../../common/stores/localEvent';
@@ -32,11 +31,12 @@ interface RundownEntryProps {
export default function RundownEntry(props: RundownEntryProps) { export default function RundownEntry(props: RundownEntryProps) {
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props; const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
const { emitError } = useEmitLog(); const { emitError } = useEmitLog();
const { openId, removeOpenEvent } = useEventEditorStore();
const { addEvent, updateEvent, deleteEvent } = useEventAction(); const { addEvent, updateEvent, deleteEvent } = useEventAction();
const { moveCursorTo } = useContext(CursorContext);
const { eventSettings } = useLocalEvent(); const openId = useEventEditorStore((state) => state.openId);
const removeOpenEvent = useEventEditorStore((state) => state.removeOpenEvent);
const eventSettings = useLocalEvent((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
@@ -45,90 +45,73 @@ export default function RundownEntry(props: RundownEntryProps) {
field: keyof Omit<OntimeEvent, 'duration'> | 'durationOverride'; field: keyof Omit<OntimeEvent, 'duration'> | 'durationOverride';
value: unknown; 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: SupportedEvent.Event };
const options = {
startTimeIsLastEnd,
defaultPublic,
lastEventId: previousEventId,
after: data.id,
};
addEvent(newEvent, options);
break;
}
case 'delay': {
addEvent({ type: SupportedEvent.Delay }, { after: data.id });
break;
}
case 'block': {
addEvent({ type: SupportedEvent.Block }, { after: data.id });
break;
}
case 'delete': {
if (openId === data.id) {
removeOpenEvent();
}
deleteEvent(data.id);
break;
}
case 'clone': {
const newEvent = cloneEvent(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 === 'durationOverride' && data.type === SupportedEvent.Event) { // we assume the data is not changing in the lifecycle of this component
// duration defines timeEnd // changes to the data would make rundown re-render, also re-rendering this component
newData.duration = value as number; const actionHandler = useCallback((action: EventItemActions, payload?: number | FieldValue) => {
newData.timeEnd = data.timeStart + (value as number); switch (action) {
updateEvent(newData); case 'event': {
} else if (field === 'timeStart' && data.type === SupportedEvent.Event) { const newEvent = { type: SupportedEvent.Event };
newData.duration = calculateDuration(value as number, data.timeEnd); const options = {
newData.timeStart = value as number; startTimeIsLastEnd,
updateEvent(newData); defaultPublic,
} else if (field === 'timeEnd' && data.type === SupportedEvent.Event) { lastEventId: previousEventId,
newData.duration = calculateDuration(data.timeStart, value as number); after: data.id,
newData.timeEnd = value as number; };
updateEvent(newData); addEvent(newEvent, options);
} else if (field in data) { break;
// @ts-expect-error not sure how to type this
newData[field] = value;
updateEvent(newData);
} else {
emitError(`Unknown field: ${field}`);
}
break;
}
default:
emitError(`Unknown action called: ${action}`);
break;
} }
}, case 'delay': {
[ addEvent({ type: SupportedEvent.Delay }, { after: data.id });
addEvent, break;
data, }
defaultPublic, case 'block': {
deleteEvent, addEvent({ type: SupportedEvent.Block }, { after: data.id });
emitError, break;
moveCursorTo, }
openId, case 'delete': {
previousEventId, if (openId === data.id) {
removeOpenEvent, removeOpenEvent();
startTimeIsLastEnd, }
updateEvent, deleteEvent(data.id);
], break;
); }
case 'clone': {
const newEvent = cloneEvent(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 === 'durationOverride' && data.type === SupportedEvent.Event) {
// duration defines timeEnd
newData.duration = value as number;
newData.timeEnd = data.timeStart + (value as number);
updateEvent(newData);
} else if (field === 'timeStart' && data.type === SupportedEvent.Event) {
newData.duration = calculateDuration(value as number, data.timeEnd);
newData.timeStart = value as number;
updateEvent(newData);
} else if (field === 'timeEnd' && data.type === SupportedEvent.Event) {
newData.duration = calculateDuration(data.timeStart, value as number);
newData.timeEnd = value as number;
updateEvent(newData);
} else if (field in data) {
// @ts-expect-error not sure how to type this
newData[field] = value;
updateEvent(newData);
} else {
emitError(`Unknown field: ${field}`);
}
break;
}
default:
throw new Error(`Unhandled event ${action}`);
}
}, []);
if (data.type === SupportedEvent.Event) { if (data.type === SupportedEvent.Event) {
return ( return (
@@ -154,11 +137,9 @@ export default function RundownEntry(props: RundownEntryProps) {
/> />
); );
} else if (data.type === SupportedEvent.Block) { } else if (data.type === SupportedEvent.Block) {
// @ts-expect-error -- revise types here return <BlockBlock data={data} hasCursor={hasCursor} actionHandler={actionHandler} />;
return <BlockBlock index={index} data={data} hasCursor={hasCursor} actionHandler={actionHandler} />;
} else if (data.type === SupportedEvent.Delay) { } else if (data.type === SupportedEvent.Delay) {
// @ts-expect-error -- revise types here return <DelayBlock data={data} hasCursor={hasCursor} actionHandler={actionHandler} />;
return <DelayBlock index={index} data={data} hasCursor={hasCursor} actionHandler={actionHandler} />;
} }
return null; return null;
} }
@@ -1,25 +1,23 @@
import { memo } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'; import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import { CursorProvider } from '../../common/context/CursorContext';
import { handleLinks } from '../../common/utils/linkUtils'; import { handleLinks } from '../../common/utils/linkUtils';
import RundownWrapper from './RundownWrapper'; import RundownWrapper from './RundownWrapper';
import style from '../editors/Editor.module.scss'; import style from '../editors/Editor.module.scss';
export default function RundownExport() { const RundownExport = () => {
return ( return (
<CursorProvider> <Box className={style.editor} data-testid='panel-rundown'>
<Box className={style.editor} data-testid='panel-rundown'> <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'rundown')} />
<IoArrowUp <ErrorBoundary>
className={style.corner} <RundownWrapper />
onClick={(event) => handleLinks(event, 'rundown')} </ErrorBoundary>
/> </Box>
<ErrorBoundary>
<RundownWrapper />
</ErrorBoundary>
</Box>
</CursorProvider>
); );
} };
export default memo(RundownExport);
@@ -13,11 +13,7 @@ export default function RundownWrapper() {
<> <>
<RundownMenu /> <RundownMenu />
<div className={styles.content}> <div className={styles.content}>
{status === 'success' && data ? ( {status === 'success' && data ? <Rundown entries={data} /> : <Empty text='Connecting to server' />}
<Rundown entries={data} />
) : (
<Empty text='Connecting to server' />
)}
</div> </div>
</> </>
); );
@@ -19,6 +19,7 @@ $block-cursor-color: $blue-400;
font-family: $ontime-font-family; font-family: $ontime-font-family;
border-radius: $block-border-radius; border-radius: $block-border-radius;
margin: 4px 2px; margin: 4px 2px;
position: relative;
} }
@mixin block-spacing() { @mixin block-spacing() {
@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { OntimeBlock, OntimeEvent } from 'ontime-types'; import { OntimeBlock, OntimeEvent } from 'ontime-types';
@@ -10,43 +11,53 @@ import { EventItemActions } from '../RundownEntry';
import style from './BlockBlock.module.scss'; import style from './BlockBlock.module.scss';
interface BlockBlockProps { interface BlockBlockProps {
index: number;
data: OntimeBlock; data: OntimeBlock;
hasCursor: boolean; hasCursor: boolean;
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void; actionHandler: (
action: EventItemActions,
payload?:
| number
| {
field: keyof Omit<OntimeEvent, 'duration'> | 'durationOverride';
value: unknown;
},
) => void;
} }
export default function BlockBlock(props: BlockBlockProps) { export default function BlockBlock(props: BlockBlockProps) {
const { index, data, hasCursor, actionHandler } = props; const { data, hasCursor, actionHandler } = props;
const onFocusRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
const {
attributes: dragAttributes,
listeners: dragListeners,
setNodeRef,
transform,
transition,
} = useSortable({
id: data.id,
animateLayoutChanges: () => false,
});
const dragStyle = {
transform: CSS.Translate.toString(transform),
transition,
};
useEffect(() => { useEffect(() => {
if (hasCursor) { if (hasCursor) {
onFocusRef?.current?.focus(); handleRef?.current?.focus();
} }
}, [hasCursor]) }, [hasCursor]);
const blockClasses = cx([ const blockClasses = cx([style.block, hasCursor ? style.hasCursor : null]);
style.block,
hasCursor ? style.hasCursor : null,
]);
return ( return (
<Draggable key={data.id} draggableId={data.id} index={index}> <div className={blockClasses} ref={setNodeRef} style={dragStyle}>
{(provided) => ( <span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<div className={blockClasses} {...provided.draggableProps} ref={provided.innerRef}> <IoReorderTwo />
<span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}> </span>
<IoReorderTwo /> <BlockActionMenu className={style.actionOverlay} showAdd showDelay enableDelete actionHandler={actionHandler} />
</span> </div>
<BlockActionMenu
className={style.actionOverlay}
showAdd
showDelay
enableDelete
actionHandler={actionHandler}
/>
</div>
)}
</Draggable>
); );
} }
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { Button, HStack } from '@chakra-ui/react'; import { Button, HStack } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark'; import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { OntimeDelay, OntimeEvent } from 'ontime-types'; import { OntimeDelay, OntimeEvent } from 'ontime-types';
@@ -16,19 +17,42 @@ import style from './DelayBlock.module.scss';
interface DelayBlockProps { interface DelayBlockProps {
data: OntimeDelay; data: OntimeDelay;
index: number;
hasCursor: boolean; hasCursor: boolean;
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent; value: unknown }) => void; actionHandler: (
action: EventItemActions,
payload?:
| number
| {
field: keyof Omit<OntimeEvent, 'duration'> | 'durationOverride';
value: unknown;
},
) => void;
} }
export default function DelayBlock(props: DelayBlockProps) { export default function DelayBlock(props: DelayBlockProps) {
const { data, index, hasCursor, actionHandler } = props; const { data, hasCursor, actionHandler } = props;
const { applyDelay, updateEvent } = useEventAction(); const { applyDelay, updateEvent } = useEventAction();
const onFocusRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
const {
attributes: dragAttributes,
listeners: dragListeners,
setNodeRef,
transform,
transition,
} = useSortable({
id: data.id,
animateLayoutChanges: () => false,
});
const dragStyle = {
transform: CSS.Translate.toString(transform),
transition,
};
useEffect(() => { useEffect(() => {
if (hasCursor) { if (hasCursor) {
onFocusRef?.current?.focus(); handleRef?.current?.focus();
} }
}, [hasCursor]); }, [hasCursor]);
@@ -53,21 +77,17 @@ export default function DelayBlock(props: DelayBlockProps) {
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined; const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
return ( return (
<Draggable key={data.id} draggableId={data.id} index={index}> <div className={blockClasses} ref={setNodeRef} style={dragStyle}>
{(provided) => ( <span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<div className={blockClasses} {...provided.draggableProps} ref={provided.innerRef}> <IoReorderTwo />
<span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}> </span>
<IoReorderTwo /> <DelayInput value={delayValue} submitHandler={delaySubmitHandler} />
</span> <HStack spacing='8px' className={style.actionOverlay}>
<DelayInput value={delayValue} submitHandler={delaySubmitHandler} /> <Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'>
<HStack spacing='8px' className={style.actionOverlay}> Apply delay
<Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'> </Button>
Apply delay <BlockActionMenu showAdd enableDelete actionHandler={actionHandler} />
</Button> </HStack>
<BlockActionMenu showAdd enableDelete actionHandler={actionHandler} /> </div>
</HStack>
</div>
)}
</Draggable>
); );
} }
@@ -1,39 +1,18 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { useSortable } from '@dnd-kit/sortable';
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react'; import { CSS } from '@dnd-kit/utilities';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { Playback } from 'ontime-types'; import { OntimeEvent, Playback } from 'ontime-types';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn'; import { useCursor } from '../../../common/stores/cursorStore';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { setEventPlayback } from '../../../common/hooks/useSocket';
import { useEventEditorStore } from '../../../common/stores/eventEditor'; import { useEventEditorStore } from '../../../common/stores/eventEditor';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import { EventItemActions } from '../RundownEntry'; import { EventItemActions } from '../RundownEntry';
import BlockActionMenu from './composite/BlockActionMenu'; import EventBlockInner from './EventBlockInner';
import EventBlockProgressBar from './composite/EventBlockProgressBar';
import EventBlockTimers from './composite/EventBlockTimers';
import style from './EventBlock.module.scss'; import style from './EventBlock.module.scss';
const blockBtnStyle = {
size: 'sm',
};
const tooltipProps = {
openDelay: tooltipDelayMid,
};
interface EventBlockProps { interface EventBlockProps {
timeStart: number; timeStart: number;
timeEnd: number; timeEnd: number;
@@ -52,7 +31,15 @@ interface EventBlockProps {
selected: boolean; selected: boolean;
hasCursor: boolean; hasCursor: boolean;
playback?: Playback; playback?: Playback;
actionHandler: (action: EventItemActions, payload?: any) => void; actionHandler: (
action: EventItemActions,
payload?:
| number
| {
field: keyof Omit<OntimeEvent, 'duration'> | 'durationOverride';
value: unknown;
},
) => void;
} }
export default function EventBlock(props: EventBlockProps) { export default function EventBlock(props: EventBlockProps) {
@@ -77,54 +64,61 @@ export default function EventBlock(props: EventBlockProps) {
actionHandler, actionHandler,
} = props; } = props;
const { openId, setOpenEvent, removeOpenEvent } = useEventEditorStore(); const moveCursorTo = useCursor((state) => state.moveCursorTo);
const { updateEvent } = useEventAction(); const handleRef = useRef<null | HTMLSpanElement>(null);
const [blockTitle, setBlockTitle] = useState<string>(title || ''); const [isVisible, setIsVisible] = useState(false);
const onFocusRef = useRef<null | HTMLSpanElement>(null); const openId = useEventEditorStore((state) => state.openId);
const {
isDragging,
attributes: dragAttributes,
listeners: dragListeners,
setNodeRef,
transform,
transition,
} = useSortable({
id: eventId,
animateLayoutChanges: () => false,
});
const dragStyle = {
zIndex: isDragging ? 2 : 'inherit',
transform: CSS.Translate.toString(transform),
transition,
};
const binderColours = colour && getAccessibleColour(colour); const binderColours = colour && getAccessibleColour(colour);
// Todo: could I re-render the item without causing a state change here?
// ?? use refs instead?
useEffect(() => {
setBlockTitle(title);
}, [title]);
useEffect(() => { useEffect(() => {
if (hasCursor) { if (hasCursor) {
onFocusRef?.current?.focus(); handleRef?.current?.focus();
} }
}, [hasCursor]); }, [hasCursor]);
const handleTitle = useCallback( useLayoutEffect(() => {
(text: string) => { const observer = new IntersectionObserver(
if (text === title) { ([entry]) => {
return; if (entry.isIntersecting) {
} setIsVisible(true);
}
},
{
root: null,
threshold: 1,
},
);
const cleanVal = text.trim(); const handleRefCurrent = handleRef.current;
setBlockTitle(cleanVal); if (handleRefCurrent) {
observer.observe(handleRefCurrent);
updateEvent({ id: eventId, title: cleanVal });
},
[title, updateEvent, eventId],
);
const toggleOpenEvent = useCallback(() => {
if (openId === eventId) {
removeOpenEvent();
} else {
setOpenEvent(eventId);
} }
}, [eventId, openId, removeOpenEvent, setOpenEvent]);
const eventIsPlaying = selected && playback === Playback.Play; return () => {
const playBtnStyles = { _hover: {} }; if (handleRefCurrent) {
if (!skip && eventIsPlaying) { observer.unobserve(handleRefCurrent);
playBtnStyles._hover = { bg: '#c05621' }; }
} else if (!skip && !eventIsPlaying) { };
playBtnStyles._hover = {}; }, [handleRef]);
}
const blockClasses = cx([ const blockClasses = cx([
style.eventBlock, style.eventBlock,
@@ -134,118 +128,32 @@ export default function EventBlock(props: EventBlockProps) {
]); ]);
return ( return (
<Draggable key={eventId} draggableId={eventId} index={index}> <div className={blockClasses} ref={setNodeRef} style={dragStyle}>
{(provided) => ( <div className={style.binder} style={{ ...binderColours }} tabIndex={-1} onClick={() => moveCursorTo(index)}>
<div className={blockClasses} {...provided.draggableProps} ref={provided.innerRef}> <span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<div <IoReorderTwo />
className={style.binder} </span>
style={{ ...binderColours }} {eventIndex}
tabIndex={-1} </div>
onClick={() => actionHandler('set-cursor', index)} {isVisible && (
> <EventBlockInner
<span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}> isOpen={openId === eventId}
<IoReorderTwo /> timeStart={timeStart}
</span> timeEnd={timeEnd}
{eventIndex} duration={duration}
</div> eventId={eventId}
<div className={style.playbackActions}> isPublic={isPublic}
<TooltipActionBtn title={title}
variant='ontime-subtle-white' note={note}
aria-label='Skip event' delay={delay}
tooltip='Skip event' previousEnd={previousEnd}
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />} next={next}
{...tooltipProps} skip={skip}
{...blockBtnStyle} selected={selected}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })} playback={playback}
tabIndex={-1} actionHandler={actionHandler}
disabled={selected} />
/>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Load event'
tooltip='Load event'
icon={<IoReload className={style.flip} />}
disabled={skip}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)}
tabIndex={-1}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Start event'
tooltip='Start event'
icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />}
disabled={skip}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.startEvent(eventId)}
backgroundColor={eventIsPlaying ? '#58A151' : undefined}
_hover={{ backgroundColor: eventIsPlaying ? '#58A151' : undefined }}
tabIndex={-1}
/>
</div>
<EventBlockTimers
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
delay={delay}
actionHandler={actionHandler}
previousEnd={previousEnd}
/>
<Editable
variant='ontime'
value={blockTitle}
className={`${style.eventTitle} ${!title ? style.noTitle : ''}`}
placeholder='Event title'
onChange={(value) => setBlockTitle(value)}
onSubmit={(value) => handleTitle(value)}
>
<EditablePreview className={style.preview} />
<EditableInput />
</Editable>
<div className={style.statusElements}>
<span className={style.eventNote}>{note}</span>
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
<EventBlockProgressBar playback={playback} />
</div>
<div className={style.eventStatus} tabIndex={-1}>
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}>
<span>
<IoPlaySkipForward className={`${style.statusIcon} ${next ? style.active : ''}`} />
</span>
</Tooltip>
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
<span>
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} />
</span>
</Tooltip>
</div>
</div>
<div className={style.eventActions}>
<TooltipActionBtn
{...blockBtnStyle}
variant='ontime-subtle-white'
size='sm'
icon={<IoOptions />}
clickHandler={toggleOpenEvent}
tooltip='Event options'
aria-label='Event options'
tabIndex={-1}
backgroundColor={openId === eventId ? '#2B5ABC' : undefined}
color={openId === eventId ? 'white' : '#f6f6f6'}
/>
<BlockActionMenu
showAdd
showDelay
showBlock
showClone
enableDelete={!selected}
actionHandler={actionHandler}
/>
</div>
</div>
)} )}
</Draggable> </div>
); );
} }
@@ -0,0 +1,214 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { Playback } from 'ontime-types';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { setEventPlayback } from '../../../common/hooks/useSocket';
import { useEventEditorStore } from '../../../common/stores/eventEditor';
import { tooltipDelayMid } from '../../../ontimeConfig';
import { EventItemActions } from '../RundownEntry';
import BlockActionMenu from './composite/BlockActionMenu';
import EventBlockProgressBar from './composite/EventBlockProgressBar';
import EventBlockTimers from './composite/EventBlockTimers';
import style from './EventBlock.module.scss';
const blockBtnStyle = {
size: 'sm',
};
const tooltipProps = {
openDelay: tooltipDelayMid,
};
interface EventBlockInnerProps {
isOpen: boolean;
timeStart: number;
timeEnd: number;
duration: number;
eventId: string;
isPublic: boolean;
title: string;
note: string;
delay: number;
previousEnd: number;
next: boolean;
skip: boolean;
selected: boolean;
playback?: Playback;
actionHandler: (action: EventItemActions, payload?: any) => void;
}
const EventBlockInner = (props: EventBlockInnerProps) => {
const {
isOpen,
timeStart,
timeEnd,
duration,
eventId,
isPublic = true,
title,
note,
delay,
previousEnd,
next,
skip = false,
selected,
playback,
actionHandler,
} = props;
const { updateEvent } = useEventAction();
const [blockTitle, setBlockTitle] = useState<string>(title || '');
const [renderInner, setRenderInner] = useState(false);
const setOpenEvent = useEventEditorStore((state) => state.setOpenEvent);
const removeOpenEvent = useEventEditorStore((state) => state.removeOpenEvent);
// Todo: could I re-render the item without causing a state change here?
// ?? use refs instead?
useEffect(() => {
setRenderInner(true);
}, []);
useEffect(() => {
setBlockTitle(title);
}, [title]);
const handleTitle = useCallback(
(text: string) => {
if (text === title) {
return;
}
const cleanVal = text.trim();
setBlockTitle(cleanVal);
updateEvent({ id: eventId, title: cleanVal });
},
[title, updateEvent, eventId],
);
const toggleOpenEvent = useCallback(() => {
if (isOpen) {
removeOpenEvent();
} else {
setOpenEvent(eventId);
}
}, [eventId, isOpen, removeOpenEvent, setOpenEvent]);
const eventIsPlaying = selected && playback === Playback.Play;
const playBtnStyles = { _hover: {} };
if (!skip && eventIsPlaying) {
playBtnStyles._hover = { bg: '#c05621' };
} else if (!skip && !eventIsPlaying) {
playBtnStyles._hover = {};
}
return !renderInner ? null : (
<>
<div className={style.playbackActions}>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Skip event'
tooltip='Skip event'
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
tabIndex={-1}
disabled={selected}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Load event'
tooltip='Load event'
icon={<IoReload className={style.flip} />}
disabled={skip}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)}
tabIndex={-1}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Start event'
tooltip='Start event'
icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />}
disabled={skip}
{...tooltipProps}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.startEvent(eventId)}
backgroundColor={eventIsPlaying ? '#58A151' : undefined}
_hover={{ backgroundColor: eventIsPlaying ? '#58A151' : undefined }}
tabIndex={-1}
/>
</div>
<EventBlockTimers
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
delay={delay}
actionHandler={actionHandler}
previousEnd={previousEnd}
/>
<Editable
variant='ontime'
value={blockTitle}
className={`${style.eventTitle} ${!title ? style.noTitle : ''}`}
placeholder='Event title'
onChange={(value) => setBlockTitle(value)}
onSubmit={(value) => handleTitle(value)}
>
<EditablePreview className={style.preview} />
<EditableInput />
</Editable>
<div className={style.statusElements}>
<span className={style.eventNote}>{note}</span>
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
{selected && <EventBlockProgressBar playback={playback} />}
</div>
<div className={style.eventStatus} tabIndex={-1}>
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}>
<span>
<IoPlaySkipForward className={`${style.statusIcon} ${next ? style.active : ''}`} />
</span>
</Tooltip>
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
<span>
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} />
</span>
</Tooltip>
</div>
</div>
<div className={style.eventActions}>
<TooltipActionBtn
{...blockBtnStyle}
variant='ontime-subtle-white'
size='sm'
icon={<IoOptions />}
clickHandler={toggleOpenEvent}
tooltip='Event options'
aria-label='Event options'
tabIndex={-1}
backgroundColor={isOpen ? '#2B5ABC' : undefined}
color={isOpen ? 'white' : '#f6f6f6'}
/>
<BlockActionMenu showAdd showDelay showBlock showClone enableDelete={!selected} actionHandler={actionHandler} />
</div>
</>
);
};
export default memo(EventBlockInner);
@@ -25,7 +25,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
const doStartTime = useRef<HTMLInputElement | null>(null); const doStartTime = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null); const doPublic = useRef<HTMLInputElement | null>(null);
const { eventSettings } = useLocalEvent(); const eventSettings = useLocalEvent((state) => state.eventSettings);
const defaultPublic = eventSettings.defaultPublic; const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
@@ -148,7 +148,7 @@ export default function OntimeTable({ tableData, userFields, selectedId, handleU
if (el) { if (el) {
el.scrollIntoView({ el.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'center', block: 'start',
inline: 'nearest', inline: 'nearest',
}); });
} }
@@ -13,22 +13,15 @@ export default function SortableCell({ column }) {
id: column.id, id: column.id,
}); });
// prevent scaling on drag
const cssTransform = {
...transform,
scaleX: 1,
scaleY: 1,
}
// build drag styles // build drag styles
const dragStyle = { const dragStyle = {
transform: CSS.Transform.toString(cssTransform),
transition,
...style, ...style,
transform: CSS.Translate.toString(transform),
transition,
}; };
return ( return (
<th {...restColumn} ref={setNodeRef} style={{...dragStyle}} className={isDragging ? styles.dragging: ''}> <th {...restColumn} ref={setNodeRef} style={dragStyle} className={isDragging ? styles.dragging : ''}>
<div {...attributes} {...listeners}> <div {...attributes} {...listeners}>
<Tooltip label={column.Header} openDelay={tooltipDelayFast}> <Tooltip label={column.Header} openDelay={tooltipDelayFast}>
{column.render('Header')} {column.render('Header')}
@@ -12,7 +12,7 @@ export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
const withData = (Component: ReactNode) => { const withData = (Component: ReactNode) => {
return (props) => { return (props) => {
// persisted app state // persisted app state
const { mirror: isMirrored } = useViewOptionsStore(); const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data // HTTP API data
const { data: eventsData } = useRundown(); const { data: eventsData } = useRundown();
+1 -1
View File
@@ -1,6 +1,6 @@
export const ontimeMenuOnDark = { export const ontimeMenuOnDark = {
list: { list: {
borderRadius: "3px", borderRadius: '3px',
border: 'none', border: 'none',
bg: '#fff', // $gray-50 bg: '#fff', // $gray-50
zIndex: 100, zIndex: 100,
+17 -105
View File
@@ -31,8 +31,8 @@ importers:
apps/client: apps/client:
specifiers: specifiers:
'@chakra-ui/react': ^2.5.1 '@chakra-ui/react': ^2.5.1
'@dnd-kit/core': ^6.0.6 '@dnd-kit/core': ^6.0.8
'@dnd-kit/sortable': ^7.0.1 '@dnd-kit/sortable': ^7.0.2
'@dnd-kit/utilities': ^3.2.1 '@dnd-kit/utilities': ^3.2.1
'@emotion/react': ^11.10.5 '@emotion/react': ^11.10.5
'@emotion/styled': ^11.10.5 '@emotion/styled': ^11.10.5
@@ -50,7 +50,6 @@ importers:
'@types/luxon': ^3.2.0 '@types/luxon': ^3.2.0
'@types/prop-types': ^15.7.5 '@types/prop-types': ^15.7.5
'@types/react': ^18.0.26 '@types/react': ^18.0.26
'@types/react-beautiful-dnd': ^13.1.3
'@types/react-dom': ^18.0.10 '@types/react-dom': ^18.0.10
'@types/testing-library__jest-dom': ^5.14.5 '@types/testing-library__jest-dom': ^5.14.5
'@typescript-eslint/eslint-plugin': ^5.48.1 '@typescript-eslint/eslint-plugin': ^5.48.1
@@ -77,7 +76,6 @@ importers:
prettier: ^2.8.3 prettier: ^2.8.3
prop-types: ^15.8.1 prop-types: ^15.8.1
react: ^18.2.0 react: ^18.2.0
react-beautiful-dnd: ^13.1.1
react-dom: ^18.2.0 react-dom: ^18.2.0
react-fast-compare: ^3.2.0 react-fast-compare: ^3.2.0
react-hook-form: ^7.43.5 react-hook-form: ^7.43.5
@@ -97,8 +95,8 @@ importers:
zustand: ^4.3.6 zustand: ^4.3.6
dependencies: dependencies:
'@chakra-ui/react': 2.5.1_loo4skotrnm7icurwgkplqpnwq '@chakra-ui/react': 2.5.1_loo4skotrnm7icurwgkplqpnwq
'@dnd-kit/core': 6.0.7_biqbaboplfbrettd7655fr4n2y '@dnd-kit/core': 6.0.8_biqbaboplfbrettd7655fr4n2y
'@dnd-kit/sortable': 7.0.2_pmudlfv2z3i7vvlookxjkeidxe '@dnd-kit/sortable': 7.0.2_52scne4zmdeyjh2otzkgz2xfvu
'@dnd-kit/utilities': 3.2.1_react@18.2.0 '@dnd-kit/utilities': 3.2.1_react@18.2.0
'@emotion/react': 11.10.5_kzbn2opkn2327fwg5yzwzya5o4 '@emotion/react': 11.10.5_kzbn2opkn2327fwg5yzwzya5o4
'@emotion/styled': 11.10.5_qvatmowesywn4ye42qoh247szu '@emotion/styled': 11.10.5_qvatmowesywn4ye42qoh247szu
@@ -115,7 +113,6 @@ importers:
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
luxon: 3.3.0 luxon: 3.3.0
react: 18.2.0 react: 18.2.0
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-fast-compare: 3.2.0 react-fast-compare: 3.2.0
react-hook-form: 7.43.5_react@18.2.0 react-hook-form: 7.43.5_react@18.2.0
@@ -135,7 +132,6 @@ importers:
'@types/luxon': 3.2.0 '@types/luxon': 3.2.0
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
'@types/react': 18.0.26 '@types/react': 18.0.26
'@types/react-beautiful-dnd': 13.1.3
'@types/react-dom': 18.0.10 '@types/react-dom': 18.0.10
'@types/testing-library__jest-dom': 5.14.5 '@types/testing-library__jest-dom': 5.14.5
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse '@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
@@ -1632,11 +1628,11 @@ packages:
react: '>=16.8.0' react: '>=16.8.0'
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@dnd-kit/core/6.0.7_biqbaboplfbrettd7655fr4n2y: /@dnd-kit/core/6.0.8_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-qcLBTVTjmLuLqC0RHQ+dFKN5neWmAI56H9xZ+he9WEJEkAvR76YAcz7DSWDJfjErepfG2H3Fkb9lYiX7cPR62g==} resolution: {integrity: sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==}
peerDependencies: peerDependencies:
react: '>=16.8.0' react: '>=16.8.0'
react-dom: '>=16.8.0' react-dom: '>=16.8.0'
@@ -1645,19 +1641,19 @@ packages:
'@dnd-kit/utilities': 3.2.1_react@18.2.0 '@dnd-kit/utilities': 3.2.1_react@18.2.0
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@dnd-kit/sortable/7.0.2_pmudlfv2z3i7vvlookxjkeidxe: /@dnd-kit/sortable/7.0.2_52scne4zmdeyjh2otzkgz2xfvu:
resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==}
peerDependencies: peerDependencies:
'@dnd-kit/core': ^6.0.7 '@dnd-kit/core': ^6.0.7
react: '>=16.8.0' react: '>=16.8.0'
dependencies: dependencies:
'@dnd-kit/core': 6.0.7_biqbaboplfbrettd7655fr4n2y '@dnd-kit/core': 6.0.8_biqbaboplfbrettd7655fr4n2y
'@dnd-kit/utilities': 3.2.1_react@18.2.0 '@dnd-kit/utilities': 3.2.1_react@18.2.0
react: 18.2.0 react: 18.2.0
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@dnd-kit/utilities/3.2.1_react@18.2.0: /@dnd-kit/utilities/3.2.1_react@18.2.0:
@@ -2379,7 +2375,7 @@ packages:
'@motionone/easing': 10.15.1 '@motionone/easing': 10.15.1
'@motionone/types': 10.15.1 '@motionone/types': 10.15.1
'@motionone/utils': 10.15.1 '@motionone/utils': 10.15.1
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@motionone/dom/10.15.5: /@motionone/dom/10.15.5:
@@ -2390,14 +2386,14 @@ packages:
'@motionone/types': 10.15.1 '@motionone/types': 10.15.1
'@motionone/utils': 10.15.1 '@motionone/utils': 10.15.1
hey-listen: 1.0.8 hey-listen: 1.0.8
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@motionone/easing/10.15.1: /@motionone/easing/10.15.1:
resolution: {integrity: sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==} resolution: {integrity: sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==}
dependencies: dependencies:
'@motionone/utils': 10.15.1 '@motionone/utils': 10.15.1
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@motionone/generators/10.15.1: /@motionone/generators/10.15.1:
@@ -2405,7 +2401,7 @@ packages:
dependencies: dependencies:
'@motionone/types': 10.15.1 '@motionone/types': 10.15.1
'@motionone/utils': 10.15.1 '@motionone/utils': 10.15.1
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@motionone/types/10.15.1: /@motionone/types/10.15.1:
@@ -2417,7 +2413,7 @@ packages:
dependencies: dependencies:
'@motionone/types': 10.15.1 '@motionone/types': 10.15.1
hey-listen: 1.0.8 hey-listen: 1.0.8
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@nodelib/fs.scandir/2.1.5: /@nodelib/fs.scandir/2.1.5:
@@ -2959,13 +2955,6 @@ packages:
dev: true dev: true
optional: true optional: true
/@types/hoist-non-react-statics/3.3.1:
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
dependencies:
'@types/react': 18.0.26
hoist-non-react-statics: 3.3.2
dev: false
/@types/istanbul-lib-coverage/2.0.4: /@types/istanbul-lib-coverage/2.0.4:
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
dev: true dev: true
@@ -3069,27 +3058,12 @@ packages:
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
dev: true dev: true
/@types/react-beautiful-dnd/13.1.3:
resolution: {integrity: sha512-BNdmvONKtsrZq3AGrujECQrIn8cDT+fZsxBLXuX3YWY/nHfZinUFx4W88eS0rkcXzuLbXpKOsu/1WCMPMLEpPg==}
dependencies:
'@types/react': 18.0.26
dev: true
/@types/react-dom/18.0.10: /@types/react-dom/18.0.10:
resolution: {integrity: sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==} resolution: {integrity: sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==}
dependencies: dependencies:
'@types/react': 18.0.26 '@types/react': 18.0.26
dev: true dev: true
/@types/react-redux/7.1.25:
resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==}
dependencies:
'@types/hoist-non-react-statics': 3.3.1
'@types/react': 18.0.26
hoist-non-react-statics: 3.3.2
redux: 4.2.0
dev: false
/@types/react/18.0.26: /@types/react/18.0.26:
resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==}
dependencies: dependencies:
@@ -6508,10 +6482,6 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: false dev: false
/memoize-one/5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
dev: false
/meow/9.0.0: /meow/9.0.0:
resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -7308,10 +7278,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/raf-schd/4.0.3:
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
dev: false
/random-bytes/1.0.0: /random-bytes/1.0.0:
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -7332,25 +7298,6 @@ packages:
unpipe: 1.0.0 unpipe: 1.0.0
dev: false dev: false
/react-beautiful-dnd/13.1.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
peerDependencies:
react: ^16.8.5 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.20.7
css-box-model: 1.2.1
memoize-one: 5.2.1
raf-schd: 4.0.3
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-redux: 7.2.9_biqbaboplfbrettd7655fr4n2y
redux: 4.2.0
use-memo-one: 1.1.3_react@18.2.0
transitivePeerDependencies:
- react-native
dev: false
/react-clientside-effect/1.2.6_react@18.2.0: /react-clientside-effect/1.2.6_react@18.2.0:
resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==}
peerDependencies: peerDependencies:
@@ -7406,6 +7353,7 @@ packages:
/react-is/17.0.2: /react-is/17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: true
/react-is/18.2.0: /react-is/18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
@@ -7425,28 +7373,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/react-redux/7.2.9_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
peerDependencies:
react: ^16.8.3 || ^17 || ^18
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.20.7
'@types/react-redux': 7.1.25
hoist-non-react-statics: 3.3.2
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-is: 17.0.2
dev: false
/react-refresh/0.14.0: /react-refresh/0.14.0:
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -7614,12 +7540,6 @@ packages:
strip-indent: 3.0.0 strip-indent: 3.0.0
dev: true dev: true
/redux/4.2.0:
resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==}
dependencies:
'@babel/runtime': 7.20.7
dev: false
/regenerator-runtime/0.13.11: /regenerator-runtime/0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
@@ -8768,14 +8688,6 @@ packages:
tslib: 2.5.0 tslib: 2.5.0
dev: false dev: false
/use-memo-one/1.1.3_react@18.2.0:
resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/use-sidecar/1.1.2_kzbn2opkn2327fwg5yzwzya5o4: /use-sidecar/1.1.2_kzbn2opkn2327fwg5yzwzya5o4:
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'} engines: {node: '>=10'}