diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index d2121ca4d..0a9a1f084 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -22,6 +22,7 @@ import BlockRow from './cuesheet-table-elements/BlockRow'; import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader'; import DelayRow from './cuesheet-table-elements/DelayRow'; import EventRow from './cuesheet-table-elements/EventRow'; +import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu'; import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings'; import useColumnManager from './useColumnManager'; @@ -172,7 +173,6 @@ export default function CuesheetTable(props: CuesheetTableProps) { selectedRef={isSelected ? selectedRef : undefined} skip={entry.skip} colour={entry.colour} - showModal={showModal} > {row.getVisibleCells().map((cell) => { return ( @@ -191,6 +191,7 @@ export default function CuesheetTable(props: CuesheetTableProps) { + ); } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx index bb666cb3b..b6e572dc1 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EventRow.tsx @@ -1,11 +1,11 @@ import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react'; -import { IconButton, Menu, MenuButton } from '@chakra-ui/react'; +import { IconButton } from '@chakra-ui/react'; import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal'; import Color from 'color'; import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils'; import { useCuesheetOptions } from '../../cuesheet.options'; -import CuesheetTableMenu from '../cuesheet-table-menu/CuesheetTableMenu'; +import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu'; import style from '../CuesheetTable.module.scss'; @@ -17,15 +17,16 @@ interface EventRowProps { selectedRef?: MutableRefObject; skip?: boolean; colour?: string; - showModal: (eventId: string) => void; } function EventRow(props: PropsWithChildren) { - const { children, eventId, eventIndex, rowIndex, isPast, selectedRef, skip, colour, showModal } = props; + const { children, eventId, eventIndex, rowIndex, isPast, selectedRef, skip, colour } = props; const { hideIndexColumn, showActionMenu } = useCuesheetOptions(); const ownRef = useRef(null); const [isVisible, setIsVisible] = useState(false); + const { openMenu } = useCuesheetTableMenu(); + useLayoutEffect(() => { const observer = new IntersectionObserver( ([entry]) => { @@ -64,16 +65,17 @@ function EventRow(props: PropsWithChildren) { > {showActionMenu && ( - - } - variant='ontime-subtle' - /> - - + } + variant='ontime-subtle' + onClick={(event) => { + const rect = event.currentTarget.getBoundingClientRect(); + const yPos = 8 + rect.y + rect.height / 2; + openMenu({ x: rect.x, y: yPos }, eventId, rowIndex); + }} + /> )} {!hideIndexColumn && ( diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx index ffa94caac..4d6c2055c 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx @@ -1,70 +1,35 @@ -import { MenuDivider, MenuItem, MenuList, Portal } from '@chakra-ui/react'; -import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; -import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown'; -import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; -import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline'; -import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; -import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; -import { isOntimeEvent, SupportedEvent } from 'ontime-types'; +import { memo } from 'react'; +import { Menu, MenuButton, Portal } from '@chakra-ui/react'; -import { useEventAction } from '../../../../common/hooks/useEventAction'; -import { cloneEvent } from '../../../../common/utils/eventsManager'; +import CuesheetTableMenuActionsProps from './CuesheetTableMenuActions'; +import { useCuesheetTableMenu } from './useCuesheetTableMenu'; interface CuesheetTableMenuProps { - eventId: string; - entryIndex: number; - showModal: (entryId: string) => void; + showModal: (eventId: string) => void; } -export default function CuesheetTableMenu(props: CuesheetTableMenuProps) { - const { eventId, entryIndex, showModal } = props; - const { addEvent, getEventById, reorderEvent, deleteEvent } = useEventAction(); +export default memo(CuesheetTableMenu); - const handleCloneEvent = () => { - const currentEvent = getEventById(eventId); - if (!currentEvent || !isOntimeEvent(currentEvent)) { - return; - } - - const newEvent = cloneEvent(currentEvent); - try { - addEvent(newEvent, { after: eventId }); - } catch (_error) { - // we do not handle errors here - } - }; +function CuesheetTableMenu(props: CuesheetTableMenuProps) { + const { showModal } = props; + const { isOpen, eventId, entryIndex, position, closeMenu } = useCuesheetTableMenu(); return ( - - } onClick={() => showModal(eventId)}> - Edit ... - - - } onClick={() => addEvent({ type: SupportedEvent.Event }, { before: eventId })}> - Add event above - - } onClick={() => addEvent({ type: SupportedEvent.Event }, { after: eventId })}> - Add event below - - } onClick={handleCloneEvent}> - Clone event - - - } - onClick={() => reorderEvent(eventId, entryIndex, entryIndex - 1)} - > - Move up - - } onClick={() => reorderEvent(eventId, entryIndex, entryIndex + 1)}> - Move down - - } onClick={() => deleteEvent([eventId])}> - Delete - - + {isOpen && ( + + + + + )} ); } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenuActions.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenuActions.tsx new file mode 100644 index 000000000..2932de3d8 --- /dev/null +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenuActions.tsx @@ -0,0 +1,68 @@ +import { MenuDivider, MenuItem, MenuList } from '@chakra-ui/react'; +import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; +import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown'; +import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; +import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline'; +import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; +import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; +import { isOntimeEvent, SupportedEvent } from 'ontime-types'; + +import { useEventAction } from '../../../../common/hooks/useEventAction'; +import { cloneEvent } from '../../../../common/utils/eventsManager'; + +interface CuesheetTableMenuActionsProps { + eventId: string; + entryIndex: number; + showModal: (entryId: string) => void; +} + +export default function CuesheetTableMenuActions(props: CuesheetTableMenuActionsProps) { + const { eventId, entryIndex, showModal } = props; + const { addEvent, getEventById, reorderEvent, deleteEvent } = useEventAction(); + + const handleCloneEvent = () => { + const currentEvent = getEventById(eventId); + if (!currentEvent || !isOntimeEvent(currentEvent)) { + return; + } + + const newEvent = cloneEvent(currentEvent); + try { + addEvent(newEvent, { after: eventId }); + } catch (_error) { + // we do not handle errors here + } + }; + + return ( + + } onClick={() => showModal(eventId)}> + Edit ... + + + } onClick={() => addEvent({ type: SupportedEvent.Event }, { before: eventId })}> + Add event above + + } onClick={() => addEvent({ type: SupportedEvent.Event }, { after: eventId })}> + Add event below + + } onClick={handleCloneEvent}> + Clone event + + + } + onClick={() => reorderEvent(eventId, entryIndex, entryIndex - 1)} + > + Move up + + } onClick={() => reorderEvent(eventId, entryIndex, entryIndex + 1)}> + Move down + + } onClick={() => deleteEvent([eventId])}> + Delete + + + ); +} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/useCuesheetTableMenu.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/useCuesheetTableMenu.tsx new file mode 100644 index 000000000..b531399b6 --- /dev/null +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/useCuesheetTableMenu.tsx @@ -0,0 +1,31 @@ +import { create } from 'zustand'; + +type Anchor = { x: number; y: number }; + +type OpenMenu = { + isOpen: true; + eventId: string; + entryIndex: number; +}; + +type ClosedMenu = { + isOpen: false; + eventId: null; + entryIndex: null; +}; + +type CuesheetTableMenuStore = (OpenMenu | ClosedMenu) & { + position: Anchor; + openMenu: (position: Anchor, eventId: string, entryIndex: number) => void; + closeMenu: () => void; +}; + +export const useCuesheetTableMenu = create((set) => ({ + isOpen: false, + eventId: null, + entryIndex: null, + position: { x: 0, y: 0 }, + openMenu: (position: Anchor, eventId: string, entryIndex: number) => + set({ isOpen: true, position, eventId, entryIndex }), + closeMenu: () => set({ isOpen: false }), +}));