refactor: use a single menu for table

This commit is contained in:
Carlos Valente
2025-02-17 19:37:33 +01:00
committed by Carlos Valente
parent 86bb739d90
commit 84a5b4fe5b
5 changed files with 140 additions and 73 deletions
@@ -22,6 +22,7 @@ import BlockRow from './cuesheet-table-elements/BlockRow';
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader'; import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
import DelayRow from './cuesheet-table-elements/DelayRow'; import DelayRow from './cuesheet-table-elements/DelayRow';
import EventRow from './cuesheet-table-elements/EventRow'; import EventRow from './cuesheet-table-elements/EventRow';
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings'; import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
import useColumnManager from './useColumnManager'; import useColumnManager from './useColumnManager';
@@ -172,7 +173,6 @@ export default function CuesheetTable(props: CuesheetTableProps) {
selectedRef={isSelected ? selectedRef : undefined} selectedRef={isSelected ? selectedRef : undefined}
skip={entry.skip} skip={entry.skip}
colour={entry.colour} colour={entry.colour}
showModal={showModal}
> >
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
return ( return (
@@ -191,6 +191,7 @@ export default function CuesheetTable(props: CuesheetTableProps) {
</tbody> </tbody>
</table> </table>
</div> </div>
<CuesheetTableMenu showModal={showModal} />
</> </>
); );
} }
@@ -1,11 +1,11 @@
import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react'; 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 { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
import Color from 'color'; import Color from 'color';
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils'; import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
import { useCuesheetOptions } from '../../cuesheet.options'; 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'; import style from '../CuesheetTable.module.scss';
@@ -17,15 +17,16 @@ interface EventRowProps {
selectedRef?: MutableRefObject<HTMLTableRowElement | null>; selectedRef?: MutableRefObject<HTMLTableRowElement | null>;
skip?: boolean; skip?: boolean;
colour?: string; colour?: string;
showModal: (eventId: string) => void;
} }
function EventRow(props: PropsWithChildren<EventRowProps>) { function EventRow(props: PropsWithChildren<EventRowProps>) {
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 { hideIndexColumn, showActionMenu } = useCuesheetOptions();
const ownRef = useRef<HTMLTableRowElement>(null); const ownRef = useRef<HTMLTableRowElement>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const { openMenu } = useCuesheetTableMenu();
useLayoutEffect(() => { useLayoutEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
@@ -64,16 +65,17 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
> >
{showActionMenu && ( {showActionMenu && (
<td className={style.actionColumn}> <td className={style.actionColumn}>
<Menu variant='ontime-on-dark' size='sm' isLazy> <IconButton
<MenuButton size='sm'
as={IconButton} aria-label='Options'
size='sm' icon={<IoEllipsisHorizontal />}
aria-label='Options' variant='ontime-subtle'
icon={<IoEllipsisHorizontal />} onClick={(event) => {
variant='ontime-subtle' const rect = event.currentTarget.getBoundingClientRect();
/> const yPos = 8 + rect.y + rect.height / 2;
<CuesheetTableMenu eventId={eventId} entryIndex={rowIndex} showModal={showModal} /> openMenu({ x: rect.x, y: yPos }, eventId, rowIndex);
</Menu> }}
/>
</td> </td>
)} )}
{!hideIndexColumn && ( {!hideIndexColumn && (
@@ -1,70 +1,35 @@
import { MenuDivider, MenuItem, MenuList, Portal } from '@chakra-ui/react'; import { memo } from 'react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; import { Menu, MenuButton, Portal } from '@chakra-ui/react';
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 CuesheetTableMenuActionsProps from './CuesheetTableMenuActions';
import { cloneEvent } from '../../../../common/utils/eventsManager'; import { useCuesheetTableMenu } from './useCuesheetTableMenu';
interface CuesheetTableMenuProps { interface CuesheetTableMenuProps {
eventId: string; showModal: (eventId: string) => void;
entryIndex: number;
showModal: (entryId: string) => void;
} }
export default function CuesheetTableMenu(props: CuesheetTableMenuProps) { export default memo(CuesheetTableMenu);
const { eventId, entryIndex, showModal } = props;
const { addEvent, getEventById, reorderEvent, deleteEvent } = useEventAction();
const handleCloneEvent = () => { function CuesheetTableMenu(props: CuesheetTableMenuProps) {
const currentEvent = getEventById(eventId); const { showModal } = props;
if (!currentEvent || !isOntimeEvent(currentEvent)) { const { isOpen, eventId, entryIndex, position, closeMenu } = useCuesheetTableMenu();
return;
}
const newEvent = cloneEvent(currentEvent);
try {
addEvent(newEvent, { after: eventId });
} catch (_error) {
// we do not handle errors here
}
};
return ( return (
<Portal> <Portal>
<MenuList> {isOpen && (
<MenuItem icon={<IoOptions />} onClick={() => showModal(eventId)}> <Menu isOpen size='sm' onClose={closeMenu} isLazy variant='ontime-on-dark'>
Edit ... <MenuButton
</MenuItem> position='absolute'
<MenuDivider /> left={position.x}
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { before: eventId })}> top={position.y}
Add event above pointerEvents='none'
</MenuItem> aria-hidden
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { after: eventId })}> w={1}
Add event below h={1}
</MenuItem> />
<MenuItem icon={<IoDuplicateOutline />} onClick={handleCloneEvent}> <CuesheetTableMenuActionsProps eventId={eventId} entryIndex={entryIndex} showModal={showModal} />
Clone event </Menu>
</MenuItem> )}
<MenuDivider />
<MenuItem
isDisabled={entryIndex < 1}
icon={<IoArrowUp />}
onClick={() => reorderEvent(eventId, entryIndex, entryIndex - 1)}
>
Move up
</MenuItem>
<MenuItem icon={<IoArrowDown />} onClick={() => reorderEvent(eventId, entryIndex, entryIndex + 1)}>
Move down
</MenuItem>
<MenuItem icon={<IoTrash />} onClick={() => deleteEvent([eventId])}>
Delete
</MenuItem>
</MenuList>
</Portal> </Portal>
); );
} }
@@ -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 (
<MenuList>
<MenuItem icon={<IoOptions />} onClick={() => showModal(eventId)}>
Edit ...
</MenuItem>
<MenuDivider />
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { before: eventId })}>
Add event above
</MenuItem>
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { after: eventId })}>
Add event below
</MenuItem>
<MenuItem icon={<IoDuplicateOutline />} onClick={handleCloneEvent}>
Clone event
</MenuItem>
<MenuDivider />
<MenuItem
isDisabled={entryIndex < 1}
icon={<IoArrowUp />}
onClick={() => reorderEvent(eventId, entryIndex, entryIndex - 1)}
>
Move up
</MenuItem>
<MenuItem icon={<IoArrowDown />} onClick={() => reorderEvent(eventId, entryIndex, entryIndex + 1)}>
Move down
</MenuItem>
<MenuItem icon={<IoTrash />} onClick={() => deleteEvent([eventId])}>
Delete
</MenuItem>
</MenuList>
);
}
@@ -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<CuesheetTableMenuStore>((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 }),
}));