mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-08 07:49:12 +00:00
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:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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,
|
||||||
|
|||||||
Generated
+17
-105
@@ -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'}
|
||||||
|
|||||||
Reference in New Issue
Block a user