From a1ad1d47a49a17f1d01dd46853943fbb028ccd90 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 8 Jul 2025 06:45:22 +0200 Subject: [PATCH] refactor: review action menu --- .../common/components/menu/Menu.module.scss | 70 ++++++++++++ .../src/common/components/menu/Menu.tsx | 52 +++++++++ .../components/popover/Popover.module.scss | 4 +- .../cuesheet-table-elements/BlockRow.tsx | 4 +- .../cuesheet-table-elements/CuesheetBody.tsx | 3 + .../cuesheet-table-elements/EventRow.tsx | 4 +- .../cuesheet-table-elements/MilestoneRow.tsx | 29 ++++- .../cuesheet-table-menu/CuesheetTableMenu.tsx | 100 ++++++++++-------- .../useCuesheetTableMenu.tsx | 22 +++- 9 files changed, 227 insertions(+), 61 deletions(-) create mode 100644 apps/client/src/common/components/menu/Menu.module.scss create mode 100644 apps/client/src/common/components/menu/Menu.tsx diff --git a/apps/client/src/common/components/menu/Menu.module.scss b/apps/client/src/common/components/menu/Menu.module.scss new file mode 100644 index 000000000..fa477c337 --- /dev/null +++ b/apps/client/src/common/components/menu/Menu.module.scss @@ -0,0 +1,70 @@ +.positioner { + outline: 0; +} + +.popup { + box-sizing: border-box; + padding-block: 0.25rem; + + color: $ui-white; + background-color: $gray-1250; + font-size: calc(1rem - 2px); + + border-radius: 3px; + border: 1px solid $gray-1000; + box-shadow: $box-shadow-l1; + outline: none; + + transform-origin: var(--transform-origin); + transition: + transform 150ms, + opacity 150ms; + + &[data-starting-style], + &[data-ending-style] { + opacity: 0; + transform: scale(0.9); + } +} + + +.item { + outline: 0; + cursor: default; + padding-block: 0.5rem; + padding-inline: 1rem 2rem; + + display: flex; + gap: 0.5rem; + line-height: 1em; + + svg { + color: $gray-500; + } + + &[data-disabled] { + opacity: 0.4; + cursor: not-allowed; + } + + &[data-highlighted] { + z-index: 0; + position: relative; + } + + &[data-highlighted]:not([data-disabled])::before { + content: ''; + z-index: -1; + position: absolute; + inset-block: 0; + inset-inline: 0.25rem; + border-radius: 3px; + background-color: $gray-1000; + } +} + +.separator { + margin: 0.25rem 0.75rem; + height: 1px; + background-color: $white-7; +} diff --git a/apps/client/src/common/components/menu/Menu.tsx b/apps/client/src/common/components/menu/Menu.tsx new file mode 100644 index 000000000..b28b0ab69 --- /dev/null +++ b/apps/client/src/common/components/menu/Menu.tsx @@ -0,0 +1,52 @@ +import { ReactNode } from 'react'; +import { Menu as BaseMenu } from '@base-ui-components/react/menu'; + +import style from './Menu.module.scss'; + +type MenuItemDivider = { type: 'divider' }; +type MenuItem = { + type: 'item'; + label: string; + icon?: ReactNode; + disabled?: boolean; + onClick: () => void; +}; + +interface MenuProps { + items: Array; + isOpen: boolean; + position: { x: number; y: number }; + onClose: () => void; +} + +export default function Menu({ items, isOpen, position, onClose }: MenuProps) { + return ( + { + if (!open) onClose(); + }} + > + + + + + {items.map((item, index) => { + if (item.type === 'divider') { + return ; + } + return ( + + {item.icon} {item.label} + + ); + })} + + + + + ); +} diff --git a/apps/client/src/common/components/popover/Popover.module.scss b/apps/client/src/common/components/popover/Popover.module.scss index 80e42b9f2..b01bc436e 100644 --- a/apps/client/src/common/components/popover/Popover.module.scss +++ b/apps/client/src/common/components/popover/Popover.module.scss @@ -1,16 +1,14 @@ .popup { box-sizing: border-box; padding: 1rem 1.5rem; - z-index: $zindex-dialog; color: $ui-white; background-color: $gray-1250; border-radius: 3px; box-shadow: $box-shadow-l1; - border: 2px solid $gray-1200; + border: 1px solid $gray-1000; - //width: 32rem; max-width: 90vw; transform-origin: var(--transform-origin); diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.tsx index 0fa0fef20..5eb92742f 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.tsx @@ -1,7 +1,7 @@ import { IoEllipsisHorizontal } from 'react-icons/io5'; import { useSessionStorage } from '@mantine/hooks'; import { flexRender, Table } from '@tanstack/react-table'; -import { EntryId, OntimeEntry } from 'ontime-types'; +import { EntryId, OntimeEntry, SupportedEntry } from 'ontime-types'; import IconButton from '../../../../common/components/buttons/IconButton'; import { useCurrentBlockId } from '../../../../common/hooks/useSocket'; @@ -45,7 +45,7 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t onClick={(e) => { const rect = e.currentTarget.getBoundingClientRect(); const yPos = 8 + rect.y + rect.height / 2; - openMenu({ x: rect.x, y: yPos }, blockId, rowIndex, null); + openMenu({ x: rect.x, y: yPos }, blockId, SupportedEntry.Block, rowIndex, null); }} > diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetBody.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetBody.tsx index fd29beb4d..5826d5770 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetBody.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/CuesheetBody.tsx @@ -126,10 +126,13 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB return ( ); 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 0c366ac9c..245fd6094 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 @@ -2,7 +2,7 @@ import { MutableRefObject, useEffect, useRef } from 'react'; import { IoEllipsisHorizontal } from 'react-icons/io5'; import { useSessionStorage } from '@mantine/hooks'; import { flexRender, Table } from '@tanstack/react-table'; -import { OntimeEntry, OntimeEvent, RGBColour } from 'ontime-types'; +import { OntimeEntry, OntimeEvent, RGBColour, SupportedEntry } from 'ontime-types'; import { colourToHex, cssOrHexToColour } from 'ontime-utils'; import IconButton from '../../../../common/components/buttons/IconButton'; @@ -100,7 +100,7 @@ export default function EventRow({ onClick={(e) => { const rect = e.currentTarget.getBoundingClientRect(); const yPos = 8 + rect.y + rect.height / 2; - openMenu({ x: rect.x, y: yPos }, event.id, rowIndex, event.parent); + openMenu({ x: rect.x, y: yPos }, event.id, SupportedEntry.Event, rowIndex, event.parent); }} > diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx index e3775d6ff..6c3357854 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.tsx @@ -1,29 +1,43 @@ import { IoEllipsisHorizontal } from 'react-icons/io5'; import { useSessionStorage } from '@mantine/hooks'; import { flexRender, Table } from '@tanstack/react-table'; -import { OntimeEntry } from 'ontime-types'; +import { EntryId, OntimeEntry, SupportedEntry } from 'ontime-types'; import IconButton from '../../../../common/components/buttons/IconButton'; import { cx, enDash } from '../../../../common/utils/styleUtils'; import { AppMode, sessionKeys } from '../../../../ontimeConfig'; import { usePersistedCuesheetOptions } from '../../cuesheet.options'; +import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu'; import style from './MilestoneRow.module.scss'; interface MilestoneRowProps { + entryId: EntryId; isPast: boolean; parentBgColour: string | null; + parentId: EntryId | null; rowBgColour?: string; rowId: string; + rowIndex: number; table: Table; } -export default function MilestoneRow({ isPast, parentBgColour, rowBgColour, rowId, table }: MilestoneRowProps) { +export default function MilestoneRow({ + entryId, + isPast, + parentBgColour, + parentId, + rowBgColour, + rowId, + rowIndex, + table, +}: MilestoneRowProps) { const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn); const [cuesheetMode] = useSessionStorage({ key: sessionKeys.cuesheetMode, defaultValue: AppMode.Edit, }); + const openMenu = useCuesheetTableMenu((store) => store.openMenu); return ( {cuesheetMode === AppMode.Edit && ( - undefined}> + { + const rect = e.currentTarget.getBoundingClientRect(); + const yPos = 8 + rect.y + rect.height / 2; + openMenu({ x: rect.x, y: yPos }, entryId, SupportedEntry.Milestone, rowIndex, parentId); + }} + > 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 f4e6a801a..c9d51c666 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,8 +1,8 @@ import { memo } from 'react'; import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } from 'react-icons/io5'; -import { Menu, MenuButton, MenuDivider, MenuItem, MenuList, Portal } from '@chakra-ui/react'; import { SupportedEntry } from 'ontime-types'; +import Menu from '../../../../common/components/dropdown-menu/DropdownMenu'; import { useEntryActions } from '../../../../common/hooks/useEntryAction'; import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal'; @@ -15,52 +15,58 @@ function CuesheetTableMenu() { const { addEntry, clone, deleteEntry, move } = useEntryActions(); const showModal = useCuesheetEditModal((state) => state.setEditableEntry); + if (!isOpen) { + return null; + } + return ( - - {isOpen && ( - - - - } onClick={() => showModal(entryId)}> - Edit ... - - - } - onClick={() => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId })} - > - Add event above - - } - onClick={() => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId })} - > - Add event below - - } onClick={() => clone(entryId)}> - Clone event - - - } onClick={() => move(entryId, 'up')}> - Move up - - } onClick={() => move(entryId, 'down')}> - Move down - - } onClick={() => deleteEntry([entryId])}> - Delete - - - - )} - + showModal(entryId), icon: }, + { type: 'divider' }, + { + type: 'item', + label: 'Add event above', + onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId }), + icon: , + }, + { + type: 'item', + label: 'Add event below', + onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId }), + icon: , + }, + { + type: 'item', + label: 'Clone event', + onClick: () => clone(entryId), + icon: , + }, + { type: 'divider' }, + { + type: 'item', + label: 'Move up', + onClick: () => move(entryId, 'up'), + icon: , + disabled: entryIndex < 1, + }, + { + type: 'item', + label: 'Move down', + onClick: () => move(entryId, 'down'), + icon: , + }, + { type: 'divider' }, + { + type: 'item', + label: 'Delete', + onClick: () => deleteEntry([entryId]), + icon: , + }, + ]} + position={position} + /> ); } 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 index d0eb483c2..46602141a 100644 --- 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 @@ -1,4 +1,4 @@ -import { EntryId } from 'ontime-types'; +import { EntryId, SupportedEntry } from 'ontime-types'; import { create } from 'zustand'; type Anchor = { x: number; y: number }; @@ -6,6 +6,7 @@ type Anchor = { x: number; y: number }; type OpenMenu = { isOpen: true; entryId: EntryId; + entryType: SupportedEntry; entryIndex: number; parentId: EntryId | null; }; @@ -13,23 +14,36 @@ type OpenMenu = { type ClosedMenu = { isOpen: false; entryId: null; + entryType: null; entryIndex: null; parentId: null; }; type CuesheetTableMenuStore = (OpenMenu | ClosedMenu) & { position: Anchor; - openMenu: (position: Anchor, entryId: EntryId, entryIndex: number, parentId: EntryId | null) => void; + openMenu: ( + position: Anchor, + entryId: EntryId, + entryType: SupportedEntry, + entryIndex: number, + parentId: EntryId | null, + ) => void; closeMenu: () => void; }; export const useCuesheetTableMenu = create((set) => ({ isOpen: false, entryId: null, + entryType: null, entryIndex: null, parentId: null, position: { x: 0, y: 0 }, - openMenu: (position: Anchor, entryId: EntryId, entryIndex: number, parentId: EntryId | null) => - set({ isOpen: true, position, entryId, entryIndex, parentId }), + openMenu: ( + position: Anchor, + entryId: EntryId, + entryType: SupportedEntry, + entryIndex: number, + parentId: EntryId | null, + ) => set({ isOpen: true, position, entryId, entryType, entryIndex, parentId }), closeMenu: () => set({ isOpen: false }), }));