diff --git a/apps/client/package.json b/apps/client/package.json index 167f798a4..30f6c82b5 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -4,8 +4,8 @@ "private": true, "dependencies": { "@chakra-ui/react": "^2.5.1", - "@dnd-kit/core": "^6.0.6", - "@dnd-kit/sortable": "^7.0.1", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", @@ -22,7 +22,6 @@ "framer-motion": "^8.0.2", "luxon": "^3.3.0", "react": "^18.2.0", - "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-fast-compare": "^3.2.0", "react-hook-form": "^7.43.5", @@ -67,7 +66,6 @@ "@types/luxon": "^3.2.0", "@types/prop-types": "^15.7.5", "@types/react": "^18.0.26", - "@types/react-beautiful-dnd": "^13.1.3", "@types/react-dom": "^18.0.10", "@types/testing-library__jest-dom": "^5.14.5", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/apps/client/src/common/context/CursorContext.tsx b/apps/client/src/common/context/CursorContext.tsx deleted file mode 100644 index 43a721c5f..000000000 --- a/apps/client/src/common/context/CursorContext.tsx +++ /dev/null @@ -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({ - 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 ( - - {children} - - ); -}; diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index 6f229f401..c8d4ce999 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -22,7 +22,7 @@ import { useEmitLog } from '../stores/logger'; export const useEventAction = () => { const queryClient = useQueryClient(); const { emitError } = useEmitLog(); - const { eventSettings } = useLocalEvent(); + const eventSettings = useLocalEvent((state) => state.eventSettings); const defaultPublic = eventSettings.defaultPublic; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; diff --git a/apps/client/src/common/stores/cursorStore.ts b/apps/client/src/common/stores/cursorStore.ts new file mode 100644 index 000000000..6c9679299 --- /dev/null +++ b/apps/client/src/common/stores/cursorStore.ts @@ -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()((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 })), +})); diff --git a/apps/client/src/features/control/message/MessageControlExport.jsx b/apps/client/src/features/control/message/MessageControlExport.jsx index 26fcc3a58..750e9edde 100644 --- a/apps/client/src/features/control/message/MessageControlExport.jsx +++ b/apps/client/src/features/control/message/MessageControlExport.jsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { Box } from '@chakra-ui/react'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; @@ -8,9 +9,9 @@ import MessageControl from './MessageControl'; import style from '../../editors/Editor.module.scss'; -export default function MessageControlExport() { +const MessageControlExport = () => { return ( - + handleLinks(event, 'messagecontrol')} />
@@ -19,4 +20,6 @@ export default function MessageControlExport() {
); -} +}; + +export default memo(MessageControlExport); diff --git a/apps/client/src/features/control/playback/TimerControlExport.tsx b/apps/client/src/features/control/playback/TimerControlExport.tsx index 0301647ca..22590d3db 100644 --- a/apps/client/src/features/control/playback/TimerControlExport.tsx +++ b/apps/client/src/features/control/playback/TimerControlExport.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { Box } from '@chakra-ui/react'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; @@ -8,7 +9,7 @@ import PlaybackControl from './PlaybackControl'; import style from '../../editors/Editor.module.scss'; -export default function TimerControlExport() { +const TimerControlExport = () => { return ( handleLinks(event, 'timercontrol')} /> @@ -19,4 +20,6 @@ export default function TimerControlExport() { ); -} +}; + +export default memo(TimerControlExport); diff --git a/apps/client/src/features/event-editor/EventEditorExport.tsx b/apps/client/src/features/event-editor/EventEditorExport.tsx index 0ce73b713..ec875278a 100644 --- a/apps/client/src/features/event-editor/EventEditorExport.tsx +++ b/apps/client/src/features/event-editor/EventEditorExport.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { Box, IconButton } from '@chakra-ui/react'; import { FiX } from '@react-icons/all-files/fi/FiX'; @@ -16,7 +17,7 @@ const closeBtnStyle = { _hover: { bg: '#ebedf0', color: '#333' }, }; -export default function InfoExport() { +const EventEditorExport = () => { const { openId, removeOpenEvent } = useEventEditorStore(); return ( @@ -31,4 +32,7 @@ export default function InfoExport() {
); -} +}; + +export default memo(EventEditorExport); + diff --git a/apps/client/src/features/info/InfoExport.tsx b/apps/client/src/features/info/InfoExport.tsx index a874bfe39..3b313c9c2 100644 --- a/apps/client/src/features/info/InfoExport.tsx +++ b/apps/client/src/features/info/InfoExport.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { Box } from '@chakra-ui/react'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; @@ -8,9 +9,9 @@ import Info from './Info'; import style from '../editors/Editor.module.scss'; -export default function InfoExport() { +const InfoExport = () => { return ( - + handleLinks(event, 'info')} />
@@ -20,3 +21,5 @@ export default function InfoExport() { ); } + +export default memo(InfoExport) diff --git a/apps/client/src/features/menu/RundownMenu.tsx b/apps/client/src/features/menu/RundownMenu.tsx index 47dcad255..7609a2b5d 100644 --- a/apps/client/src/features/menu/RundownMenu.tsx +++ b/apps/client/src/features/menu/RundownMenu.tsx @@ -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 { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle'; 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 { SupportedEvent } from 'ontime-types'; -import { CursorContext } from '../../common/context/CursorContext'; import { useEventAction } from '../../common/hooks/useEventAction'; +import { useCursor } from '../../common/stores/cursorStore'; import style from './RundownMenu.module.scss'; const RundownMenu = () => { - const { isCursorLocked, toggleCursorLocked } = useContext(CursorContext); + const isCursorLocked = useCursor((state) => state.isCursorLocked); + const toggleCursorLocked = useCursor((state) => state.toggleCursorLocked); + const { addEvent, deleteAllEvents } = useEventAction(); - // TODO: re-write this with stable functions - type ActionTypes = SupportedEvent | 'delete-all'; - const eventAction = useCallback( - (action: ActionTypes) => { - switch (action) { - case SupportedEvent.Event: - addEvent({ type: action }); - break; - case SupportedEvent.Delay: - addEvent({ type: action }); - break; - case SupportedEvent.Block: - addEvent({ type: action }); - break; - case 'delete-all': - deleteAllEvents(); - break; - } - }, - [addEvent, deleteAllEvents], - ); + const newEvent = useCallback(() => { + addEvent({ type: SupportedEvent.Event }); + }, [addEvent]); + + const newBlock = useCallback(() => { + addEvent({ type: SupportedEvent.Block }); + }, [addEvent]); + + const newDelay = useCallback(() => { + addEvent({ type: SupportedEvent.Delay }); + }, [addEvent]); + + const deleteAll = useCallback(() => { + deleteAllEvents(); + }, [deleteAllEvents]); return ( @@ -45,29 +41,24 @@ const RundownMenu = () => { onChange={(event) => toggleCursorLocked(event.target.checked)} variant='ontime' /> - Lock cursor to current + Follow loaded event - } - size='sm' - variant='ontime-subtle' - > + } size='sm' variant='ontime-subtle'> Event... - } onClick={() => eventAction(SupportedEvent.Event)}> + } onClick={newEvent}> Add event at start - } onClick={() => eventAction(SupportedEvent.Delay)}> + } onClick={newDelay}> Add delay at start - } onClick={() => eventAction(SupportedEvent.Block)}> + } onClick={newBlock}> Add block at start - } onClick={() => eventAction('delete-all')} color='#D20300'> + } onClick={deleteAll} color='#D20300'> Delete all events diff --git a/apps/client/src/features/modals/AppSettingsModal.jsx b/apps/client/src/features/modals/AppSettingsModal.jsx index c3a2c1659..4812bbc42 100644 --- a/apps/client/src/features/modals/AppSettingsModal.jsx +++ b/apps/client/src/features/modals/AppSettingsModal.jsx @@ -37,7 +37,9 @@ export default function AppSettingsModal() { const [submitting, setSubmitting] = useState(false); 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 [updateMessage, setUpdateMessage] = useState(Using ontime version: {version}); diff --git a/apps/client/src/features/rundown/Rundown.module.scss b/apps/client/src/features/rundown/Rundown.module.scss index fdcabbebd..9406ea75d 100644 --- a/apps/client/src/features/rundown/Rundown.module.scss +++ b/apps/client/src/features/rundown/Rundown.module.scss @@ -9,6 +9,7 @@ } .list { + overflow-x: clip; display: flex; flex-direction: column; } diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 3573a9e9f..5d53cebc0 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -1,17 +1,16 @@ -import { createRef, Fragment, useCallback, useContext, useEffect } from 'react'; -import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; -import { Button } from '@chakra-ui/react'; -import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; 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 { useRundownEditor } from '../../common/hooks/useSocket'; +import { useCursor } from '../../common/stores/cursorStore'; import { useLocalEvent } from '../../common/stores/localEvent'; import { cloneEvent } from '../../common/utils/eventsManager'; import QuickAddBlock from './quick-add-block/QuickAddBlock'; +import RundownEmpty from './RundownEmpty'; import RundownEntry from './RundownEntry'; import style from './Rundown.module.scss'; @@ -22,16 +21,24 @@ interface RundownProps { export default function Rundown(props: RundownProps) { const { entries } = props; - const data = useRundownEditor(); - const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext); - const { addEvent, reorderEvent } = useEventAction(); - const cursorRef = createRef(); + const [statefulEntries, setStatefulEntries] = useState(entries); - const { eventSettings } = useLocalEvent(); + const featureData = useRundownEditor(); + const { addEvent, reorderEvent } = useEventAction(); + const eventSettings = useLocalEvent((state) => state.eventSettings); const defaultPublic = eventSettings.defaultPublic; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; 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(); + + // DND KIT + const sensors = useSensors(useSensor(PointerSensor)); + const insertAtCursor = useCallback( (type: SupportedEvent | 'clone', cursor: number) => { if (cursor === -1) { @@ -78,11 +85,11 @@ export default function Rundown(props: RundownProps) { if (event.altKey && (!event.ctrlKey || !event.shiftKey)) { switch (event.code) { case 'ArrowDown': { - if (cursor < entries.length - 1) moveCursorDown(); + if (cursor < entries.length - 1) moveCursorTo(cursor + 1); break; } case 'ArrowUp': { - if (cursor > 0) moveCursorUp(); + if (cursor > 0) moveCursorTo(cursor - 1); break; } 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(() => { - // attach the event listener 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 () => { document.removeEventListener('keydown', handleKeyPress); }; - }, [handleKeyPress, cursor, entries, moveCursorTo]); + }, [handleKeyPress]); // when cursor moves, view should follow 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({ behavior: 'smooth', block: 'nearest', @@ -142,120 +159,105 @@ export default function Rundown(props: RundownProps) { // or cursor settings changed useEffect(() => { // and if we are locked - if (!isCursorLocked || !data?.selectedEventId) { + if (!isCursorLocked || !featureData?.selectedEventId) { return; } // move cursor let gotoIndex = -1; let found = false; - for (const e of entries) { + for (const entry of entries) { gotoIndex++; - if (e.id === data.selectedEventId) { + if (entry.id === featureData.selectedEventId) { found = true; break; } } if (found) { - // move cursor moveCursorTo(gotoIndex); } - }, [data?.selectedEventId, entries, isCursorLocked, moveCursorTo]); + }, [featureData?.selectedEventId, entries, isCursorLocked, moveCursorTo]); - const handleOnDragEnd = useCallback( - (result: DropResult) => { - // drop outside of area - if (!result?.destination) return; + const handleOnDragEnd = (event: DragEndEvent) => { + const { active, over } = event; - // no change - if (result.destination.index === result.source.index) return; - - // Call API - reorderEvent(result.draggableId, result.source.index, result.destination.index); - }, - [reorderEvent], - ); + if (over?.id) { + if (active.id !== over?.id) { + const fromIndex = active.data.current?.sortable.index; + const toIndex = over.data.current?.sortable.index; + // ugly hack to handle inconsistencies between dnd-kit and async store updates + setStatefulEntries((currentEntries) => { + return arrayMove(currentEntries, fromIndex, toIndex); + }); + reorderEvent(String(active.id), fromIndex, toIndex); + } + } + }; if (!entries.length) { - return ( -
- - -
- ); + return insertAtCursor(SupportedEvent.Event, -1)} />; } + let cumulativeDelay = 0; - let eventIndex = -1; let previousEnd = 0; let thisEnd = 0; let previousEventId: string | undefined; + let eventIndex = -1; return (
- - - {(provided) => ( -
- {entries.map((entry, index) => { - if (index === 0) { - cumulativeDelay = 0; - eventIndex = -1; - } - if (entry.type === 'delay' && entry.duration != null) { - cumulativeDelay += entry.duration; - } else if (entry.type === 'block') { - cumulativeDelay = 0; - } else if (entry.type === 'event') { - eventIndex++; - previousEnd = thisEnd; - thisEnd = entry.timeEnd; - previousEventId = entry.id; - } - const isLast = index === entries.length - 1; - const isSelected = data?.selectedEventId === entry.id; - const isNext = data?.nextEventId === entry.id; + + +
+ {statefulEntries.map((entry, index) => { + if (index === 0) { + cumulativeDelay = 0; + eventIndex = -1; + } + if (entry.type === SupportedEvent.Delay && entry.duration !== null) { + cumulativeDelay += entry.duration; + } else if (entry.type === SupportedEvent.Block) { + cumulativeDelay = 0; + } else if (entry.type === SupportedEvent.Event) { + eventIndex++; + previousEnd = thisEnd; + thisEnd = entry.timeEnd; + previousEventId = entry.id; + } + const isLast = index === entries.length - 1; + const isSelected = featureData?.selectedEventId === entry.id; + const isNext = featureData?.nextEventId === entry.id; - return ( - -
- -
- {((showQuickEntry && index === cursor) || isLast) && ( - - )} -
- ); - })} - {provided.placeholder} -
- )} - - + return ( +
+ + {((showQuickEntry && index === cursor) || isLast) && ( + + )} +
+ ); + })} +
+ +
); } diff --git a/apps/client/src/features/rundown/RundownEmpty.tsx b/apps/client/src/features/rundown/RundownEmpty.tsx new file mode 100644 index 000000000..6f4b20950 --- /dev/null +++ b/apps/client/src/features/rundown/RundownEmpty.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 5ecfbf6d0..41e117376 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -1,7 +1,6 @@ -import { useCallback, useContext } from 'react'; +import { useCallback } from 'react'; import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; -import { CursorContext } from '../../common/context/CursorContext'; import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventEditorStore } from '../../common/stores/eventEditor'; import { useLocalEvent } from '../../common/stores/localEvent'; @@ -32,11 +31,12 @@ interface RundownEntryProps { export default function RundownEntry(props: RundownEntryProps) { const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props; const { emitError } = useEmitLog(); - const { openId, removeOpenEvent } = useEventEditorStore(); 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 startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; @@ -45,90 +45,73 @@ export default function RundownEntry(props: RundownEntryProps) { field: keyof Omit | 'durationOverride'; 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 = { 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: - emitError(`Unknown action called: ${action}`); - break; + // we assume the data is not changing in the lifecycle of this component + // changes to the data would make rundown re-render, also re-rendering this component + const actionHandler = useCallback((action: EventItemActions, payload?: number | FieldValue) => { + switch (action) { + case 'event': { + const newEvent = { type: SupportedEvent.Event }; + const options = { + startTimeIsLastEnd, + defaultPublic, + lastEventId: previousEventId, + after: data.id, + }; + addEvent(newEvent, options); + break; } - }, - [ - addEvent, - data, - defaultPublic, - deleteEvent, - emitError, - moveCursorTo, - openId, - previousEventId, - removeOpenEvent, - startTimeIsLastEnd, - updateEvent, - ], - ); + 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 = { 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) { return ( @@ -154,11 +137,9 @@ export default function RundownEntry(props: RundownEntryProps) { /> ); } else if (data.type === SupportedEvent.Block) { - // @ts-expect-error -- revise types here - return ; + return ; } else if (data.type === SupportedEvent.Delay) { - // @ts-expect-error -- revise types here - return ; + return ; } return null; } diff --git a/apps/client/src/features/rundown/RundownExport.tsx b/apps/client/src/features/rundown/RundownExport.tsx index be8292db6..1684de707 100644 --- a/apps/client/src/features/rundown/RundownExport.tsx +++ b/apps/client/src/features/rundown/RundownExport.tsx @@ -1,25 +1,23 @@ +import { memo } from 'react'; import { Box } from '@chakra-ui/react'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; + import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'; -import { CursorProvider } from '../../common/context/CursorContext'; import { handleLinks } from '../../common/utils/linkUtils'; import RundownWrapper from './RundownWrapper'; import style from '../editors/Editor.module.scss'; -export default function RundownExport() { +const RundownExport = () => { return ( - - - handleLinks(event, 'rundown')} - /> - - - - - + + handleLinks(event, 'rundown')} /> + + + + ); -} +}; + +export default memo(RundownExport); diff --git a/apps/client/src/features/rundown/RundownWrapper.tsx b/apps/client/src/features/rundown/RundownWrapper.tsx index 1f28de1b4..eb6b5e7f5 100644 --- a/apps/client/src/features/rundown/RundownWrapper.tsx +++ b/apps/client/src/features/rundown/RundownWrapper.tsx @@ -13,11 +13,7 @@ export default function RundownWrapper() { <>
- {status === 'success' && data ? ( - - ) : ( - - )} + {status === 'success' && data ? : }
); diff --git a/apps/client/src/features/rundown/_blockMixins.scss b/apps/client/src/features/rundown/_blockMixins.scss index eecf79abe..997d355db 100644 --- a/apps/client/src/features/rundown/_blockMixins.scss +++ b/apps/client/src/features/rundown/_blockMixins.scss @@ -19,6 +19,7 @@ $block-cursor-color: $blue-400; font-family: $ontime-font-family; border-radius: $block-border-radius; margin: 4px 2px; + position: relative; } @mixin block-spacing() { diff --git a/apps/client/src/features/rundown/block-block/BlockBlock.tsx b/apps/client/src/features/rundown/block-block/BlockBlock.tsx index c319efcc2..dd1c7c06d 100644 --- a/apps/client/src/features/rundown/block-block/BlockBlock.tsx +++ b/apps/client/src/features/rundown/block-block/BlockBlock.tsx @@ -1,5 +1,6 @@ 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 { OntimeBlock, OntimeEvent } from 'ontime-types'; @@ -10,43 +11,53 @@ import { EventItemActions } from '../RundownEntry'; import style from './BlockBlock.module.scss'; interface BlockBlockProps { - index: number; data: OntimeBlock; hasCursor: boolean; - actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void; + actionHandler: ( + action: EventItemActions, + payload?: + | number + | { + field: keyof Omit | 'durationOverride'; + value: unknown; + }, + ) => void; } export default function BlockBlock(props: BlockBlockProps) { - const { index, data, hasCursor, actionHandler } = props; - const onFocusRef = useRef(null); + const { data, hasCursor, actionHandler } = props; + const handleRef = useRef(null); + + const { + attributes: dragAttributes, + listeners: dragListeners, + setNodeRef, + transform, + transition, + } = useSortable({ + id: data.id, + animateLayoutChanges: () => false, + }); + + const dragStyle = { + transform: CSS.Translate.toString(transform), + transition, + }; useEffect(() => { if (hasCursor) { - onFocusRef?.current?.focus(); + handleRef?.current?.focus(); } - }, [hasCursor]) + }, [hasCursor]); - const blockClasses = cx([ - style.block, - hasCursor ? style.hasCursor : null, - ]); + const blockClasses = cx([style.block, hasCursor ? style.hasCursor : null]); return ( - - {(provided) => ( -
- - - - -
- )} -
+
+ + + + +
); } diff --git a/apps/client/src/features/rundown/delay-block/DelayBlock.tsx b/apps/client/src/features/rundown/delay-block/DelayBlock.tsx index 07a748fd0..2e4c885fa 100644 --- a/apps/client/src/features/rundown/delay-block/DelayBlock.tsx +++ b/apps/client/src/features/rundown/delay-block/DelayBlock.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; -import { Draggable } from 'react-beautiful-dnd'; 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 { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { OntimeDelay, OntimeEvent } from 'ontime-types'; @@ -16,19 +17,42 @@ import style from './DelayBlock.module.scss'; interface DelayBlockProps { data: OntimeDelay; - index: number; hasCursor: boolean; - actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent; value: unknown }) => void; + actionHandler: ( + action: EventItemActions, + payload?: + | number + | { + field: keyof Omit | 'durationOverride'; + value: unknown; + }, + ) => void; } export default function DelayBlock(props: DelayBlockProps) { - const { data, index, hasCursor, actionHandler } = props; + const { data, hasCursor, actionHandler } = props; const { applyDelay, updateEvent } = useEventAction(); - const onFocusRef = useRef(null); + const handleRef = useRef(null); + + const { + attributes: dragAttributes, + listeners: dragListeners, + setNodeRef, + transform, + transition, + } = useSortable({ + id: data.id, + animateLayoutChanges: () => false, + }); + + const dragStyle = { + transform: CSS.Translate.toString(transform), + transition, + }; useEffect(() => { if (hasCursor) { - onFocusRef?.current?.focus(); + handleRef?.current?.focus(); } }, [hasCursor]); @@ -53,21 +77,17 @@ export default function DelayBlock(props: DelayBlockProps) { const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined; return ( - - {(provided) => ( -
- - - - - - - - -
- )} -
+
+ + + + + + + + +
); } diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 082757a80..b2910c1a3 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -1,39 +1,18 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { Draggable } from 'react-beautiful-dnd'; -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 { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; 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 { useEventAction } from '../../../common/hooks/useEventAction'; -import { setEventPlayback } from '../../../common/hooks/useSocket'; +import { useCursor } from '../../../common/stores/cursorStore'; import { useEventEditorStore } from '../../../common/stores/eventEditor'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; -import { tooltipDelayMid } from '../../../ontimeConfig'; import { EventItemActions } from '../RundownEntry'; -import BlockActionMenu from './composite/BlockActionMenu'; -import EventBlockProgressBar from './composite/EventBlockProgressBar'; -import EventBlockTimers from './composite/EventBlockTimers'; +import EventBlockInner from './EventBlockInner'; import style from './EventBlock.module.scss'; -const blockBtnStyle = { - size: 'sm', -}; - -const tooltipProps = { - openDelay: tooltipDelayMid, -}; - interface EventBlockProps { timeStart: number; timeEnd: number; @@ -52,7 +31,15 @@ interface EventBlockProps { selected: boolean; hasCursor: boolean; playback?: Playback; - actionHandler: (action: EventItemActions, payload?: any) => void; + actionHandler: ( + action: EventItemActions, + payload?: + | number + | { + field: keyof Omit | 'durationOverride'; + value: unknown; + }, + ) => void; } export default function EventBlock(props: EventBlockProps) { @@ -77,54 +64,61 @@ export default function EventBlock(props: EventBlockProps) { actionHandler, } = props; - const { openId, setOpenEvent, removeOpenEvent } = useEventEditorStore(); - const { updateEvent } = useEventAction(); - const [blockTitle, setBlockTitle] = useState(title || ''); - const onFocusRef = useRef(null); + const moveCursorTo = useCursor((state) => state.moveCursorTo); + const handleRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + 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); - // Todo: could I re-render the item without causing a state change here? - // ?? use refs instead? - useEffect(() => { - setBlockTitle(title); - }, [title]); - useEffect(() => { if (hasCursor) { - onFocusRef?.current?.focus(); + handleRef?.current?.focus(); } }, [hasCursor]); - const handleTitle = useCallback( - (text: string) => { - if (text === title) { - return; - } + useLayoutEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }, + { + root: null, + threshold: 1, + }, + ); - const cleanVal = text.trim(); - setBlockTitle(cleanVal); - - updateEvent({ id: eventId, title: cleanVal }); - }, - [title, updateEvent, eventId], - ); - - const toggleOpenEvent = useCallback(() => { - if (openId === eventId) { - removeOpenEvent(); - } else { - setOpenEvent(eventId); + const handleRefCurrent = handleRef.current; + if (handleRefCurrent) { + observer.observe(handleRefCurrent); } - }, [eventId, openId, 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 () => { + if (handleRefCurrent) { + observer.unobserve(handleRefCurrent); + } + }; + }, [handleRef]); const blockClasses = cx([ style.eventBlock, @@ -134,118 +128,32 @@ export default function EventBlock(props: EventBlockProps) { ]); return ( - - {(provided) => ( -
-
actionHandler('set-cursor', index)} - > - - - - {eventIndex} -
-
- : } - {...tooltipProps} - {...blockBtnStyle} - clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })} - tabIndex={-1} - disabled={selected} - /> - } - disabled={skip} - {...tooltipProps} - {...blockBtnStyle} - clickHandler={() => setEventPlayback.loadEvent(eventId)} - tabIndex={-1} - /> - : } - disabled={skip} - {...tooltipProps} - {...blockBtnStyle} - clickHandler={() => setEventPlayback.startEvent(eventId)} - backgroundColor={eventIsPlaying ? '#58A151' : undefined} - _hover={{ backgroundColor: eventIsPlaying ? '#58A151' : undefined }} - tabIndex={-1} - /> -
- - setBlockTitle(value)} - onSubmit={(value) => handleTitle(value)} - > - - - -
- {note} -
- -
-
- - - - - - - - - - -
-
-
- } - clickHandler={toggleOpenEvent} - tooltip='Event options' - aria-label='Event options' - tabIndex={-1} - backgroundColor={openId === eventId ? '#2B5ABC' : undefined} - color={openId === eventId ? 'white' : '#f6f6f6'} - /> - -
-
+
+
moveCursorTo(index)}> + + + + {eventIndex} +
+ {isVisible && ( + )} - +
); } diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx new file mode 100644 index 000000000..cd9474ecf --- /dev/null +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -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(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 : ( + <> +
+ : } + {...tooltipProps} + {...blockBtnStyle} + clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })} + tabIndex={-1} + disabled={selected} + /> + } + disabled={skip} + {...tooltipProps} + {...blockBtnStyle} + clickHandler={() => setEventPlayback.loadEvent(eventId)} + tabIndex={-1} + /> + : } + disabled={skip} + {...tooltipProps} + {...blockBtnStyle} + clickHandler={() => setEventPlayback.startEvent(eventId)} + backgroundColor={eventIsPlaying ? '#58A151' : undefined} + _hover={{ backgroundColor: eventIsPlaying ? '#58A151' : undefined }} + tabIndex={-1} + /> +
+ + setBlockTitle(value)} + onSubmit={(value) => handleTitle(value)} + > + + + +
+ {note} +
+ {selected && } +
+
+ + + + + + + + + + +
+
+
+ } + clickHandler={toggleOpenEvent} + tooltip='Event options' + aria-label='Event options' + tabIndex={-1} + backgroundColor={isOpen ? '#2B5ABC' : undefined} + color={isOpen ? 'white' : '#f6f6f6'} + /> + +
+ + ); +}; + +export default memo(EventBlockInner); diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx index a1947103c..7eb3eb95e 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx @@ -25,7 +25,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) { const doStartTime = useRef(null); const doPublic = useRef(null); - const { eventSettings } = useLocalEvent(); + const eventSettings = useLocalEvent((state) => state.eventSettings); const defaultPublic = eventSettings.defaultPublic; const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd; diff --git a/apps/client/src/features/table/OntimeTable.jsx b/apps/client/src/features/table/OntimeTable.jsx index 7bcb98f6c..27927c05d 100644 --- a/apps/client/src/features/table/OntimeTable.jsx +++ b/apps/client/src/features/table/OntimeTable.jsx @@ -148,7 +148,7 @@ export default function OntimeTable({ tableData, userFields, selectedId, handleU if (el) { el.scrollIntoView({ behavior: 'smooth', - block: 'center', + block: 'start', inline: 'nearest', }); } diff --git a/apps/client/src/features/table/tableElements/SortableCell.jsx b/apps/client/src/features/table/tableElements/SortableCell.jsx index 7ad3f357c..49570e0a8 100644 --- a/apps/client/src/features/table/tableElements/SortableCell.jsx +++ b/apps/client/src/features/table/tableElements/SortableCell.jsx @@ -13,22 +13,15 @@ export default function SortableCell({ column }) { id: column.id, }); - // prevent scaling on drag - const cssTransform = { - ...transform, - scaleX: 1, - scaleY: 1, - } - // build drag styles const dragStyle = { - transform: CSS.Transform.toString(cssTransform), - transition, ...style, + transform: CSS.Translate.toString(transform), + transition, }; return ( - +
{column.render('Header')} diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx index d8ccc0263..15deca090 100644 --- a/apps/client/src/features/viewers/ViewWrapper.tsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -12,7 +12,7 @@ export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean }; const withData = (Component: ReactNode) => { return (props) => { // persisted app state - const { mirror: isMirrored } = useViewOptionsStore(); + const isMirrored = useViewOptionsStore((state) => state.mirror); // HTTP API data const { data: eventsData } = useRundown(); diff --git a/apps/client/src/theme/ontimeMenu.ts b/apps/client/src/theme/ontimeMenu.ts index 8afaff96c..b38ddebad 100644 --- a/apps/client/src/theme/ontimeMenu.ts +++ b/apps/client/src/theme/ontimeMenu.ts @@ -1,6 +1,6 @@ export const ontimeMenuOnDark = { list: { - borderRadius: "3px", + borderRadius: '3px', border: 'none', bg: '#fff', // $gray-50 zIndex: 100, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89608cea4..b02d49594 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,8 +31,8 @@ importers: apps/client: specifiers: '@chakra-ui/react': ^2.5.1 - '@dnd-kit/core': ^6.0.6 - '@dnd-kit/sortable': ^7.0.1 + '@dnd-kit/core': ^6.0.8 + '@dnd-kit/sortable': ^7.0.2 '@dnd-kit/utilities': ^3.2.1 '@emotion/react': ^11.10.5 '@emotion/styled': ^11.10.5 @@ -50,7 +50,6 @@ importers: '@types/luxon': ^3.2.0 '@types/prop-types': ^15.7.5 '@types/react': ^18.0.26 - '@types/react-beautiful-dnd': ^13.1.3 '@types/react-dom': ^18.0.10 '@types/testing-library__jest-dom': ^5.14.5 '@typescript-eslint/eslint-plugin': ^5.48.1 @@ -77,7 +76,6 @@ importers: prettier: ^2.8.3 prop-types: ^15.8.1 react: ^18.2.0 - react-beautiful-dnd: ^13.1.1 react-dom: ^18.2.0 react-fast-compare: ^3.2.0 react-hook-form: ^7.43.5 @@ -97,8 +95,8 @@ importers: zustand: ^4.3.6 dependencies: '@chakra-ui/react': 2.5.1_loo4skotrnm7icurwgkplqpnwq - '@dnd-kit/core': 6.0.7_biqbaboplfbrettd7655fr4n2y - '@dnd-kit/sortable': 7.0.2_pmudlfv2z3i7vvlookxjkeidxe + '@dnd-kit/core': 6.0.8_biqbaboplfbrettd7655fr4n2y + '@dnd-kit/sortable': 7.0.2_52scne4zmdeyjh2otzkgz2xfvu '@dnd-kit/utilities': 3.2.1_react@18.2.0 '@emotion/react': 11.10.5_kzbn2opkn2327fwg5yzwzya5o4 '@emotion/styled': 11.10.5_qvatmowesywn4ye42qoh247szu @@ -115,7 +113,6 @@ importers: framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y luxon: 3.3.0 react: 18.2.0 - react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y react-dom: 18.2.0_react@18.2.0 react-fast-compare: 3.2.0 react-hook-form: 7.43.5_react@18.2.0 @@ -135,7 +132,6 @@ importers: '@types/luxon': 3.2.0 '@types/prop-types': 15.7.5 '@types/react': 18.0.26 - '@types/react-beautiful-dnd': 13.1.3 '@types/react-dom': 18.0.10 '@types/testing-library__jest-dom': 5.14.5 '@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse @@ -1632,11 +1628,11 @@ packages: react: '>=16.8.0' dependencies: react: 18.2.0 - tslib: 2.4.1 + tslib: 2.5.0 dev: false - /@dnd-kit/core/6.0.7_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-qcLBTVTjmLuLqC0RHQ+dFKN5neWmAI56H9xZ+he9WEJEkAvR76YAcz7DSWDJfjErepfG2H3Fkb9lYiX7cPR62g==} + /@dnd-kit/core/6.0.8_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' @@ -1645,19 +1641,19 @@ packages: '@dnd-kit/utilities': 3.2.1_react@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 - /@dnd-kit/sortable/7.0.2_pmudlfv2z3i7vvlookxjkeidxe: + /@dnd-kit/sortable/7.0.2_52scne4zmdeyjh2otzkgz2xfvu: resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} peerDependencies: '@dnd-kit/core': ^6.0.7 react: '>=16.8.0' 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 react: 18.2.0 - tslib: 2.4.1 + tslib: 2.5.0 dev: false /@dnd-kit/utilities/3.2.1_react@18.2.0: @@ -2379,7 +2375,7 @@ packages: '@motionone/easing': 10.15.1 '@motionone/types': 10.15.1 '@motionone/utils': 10.15.1 - tslib: 2.4.1 + tslib: 2.5.0 dev: false /@motionone/dom/10.15.5: @@ -2390,14 +2386,14 @@ packages: '@motionone/types': 10.15.1 '@motionone/utils': 10.15.1 hey-listen: 1.0.8 - tslib: 2.4.1 + tslib: 2.5.0 dev: false /@motionone/easing/10.15.1: resolution: {integrity: sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==} dependencies: '@motionone/utils': 10.15.1 - tslib: 2.4.1 + tslib: 2.5.0 dev: false /@motionone/generators/10.15.1: @@ -2405,7 +2401,7 @@ packages: dependencies: '@motionone/types': 10.15.1 '@motionone/utils': 10.15.1 - tslib: 2.4.1 + tslib: 2.5.0 dev: false /@motionone/types/10.15.1: @@ -2417,7 +2413,7 @@ packages: dependencies: '@motionone/types': 10.15.1 hey-listen: 1.0.8 - tslib: 2.4.1 + tslib: 2.5.0 dev: false /@nodelib/fs.scandir/2.1.5: @@ -2959,13 +2955,6 @@ packages: dev: 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: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} dev: true @@ -3069,27 +3058,12 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} 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: resolution: {integrity: sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==} dependencies: '@types/react': 18.0.26 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: resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} dependencies: @@ -6508,10 +6482,6 @@ packages: engines: {node: '>= 0.6'} dev: false - /memoize-one/5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - dev: false - /meow/9.0.0: resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} engines: {node: '>=10'} @@ -7308,10 +7278,6 @@ packages: engines: {node: '>=8'} dev: true - /raf-schd/4.0.3: - resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} - dev: false - /random-bytes/1.0.0: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} @@ -7332,25 +7298,6 @@ packages: unpipe: 1.0.0 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: resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: @@ -7406,6 +7353,7 @@ packages: /react-is/17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true /react-is/18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} @@ -7425,28 +7373,6 @@ packages: react: 18.2.0 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: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -7614,12 +7540,6 @@ packages: strip-indent: 3.0.0 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: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -8768,14 +8688,6 @@ packages: tslib: 2.5.0 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: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'}