mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 17:33:55 +00:00
refactor: review action menu
This commit is contained in:
committed by
Carlos Valente
parent
9281c0b51d
commit
a1ad1d47a4
@@ -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;
|
||||
}
|
||||
@@ -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<MenuItemDivider | MenuItem>;
|
||||
isOpen: boolean;
|
||||
position: { x: number; y: number };
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function Menu({ items, isOpen, position, onClose }: MenuProps) {
|
||||
return (
|
||||
<BaseMenu.Root
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose();
|
||||
}}
|
||||
>
|
||||
<BaseMenu.Trigger
|
||||
style={{ position: 'absolute', left: position.x, top: position.y, pointerEvents: 'none' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<BaseMenu.Portal>
|
||||
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8}>
|
||||
<BaseMenu.Popup className={style.popup}>
|
||||
{items.map((item, index) => {
|
||||
if (item.type === 'divider') {
|
||||
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||
}
|
||||
return (
|
||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||
{item.icon} {item.label}
|
||||
</BaseMenu.Item>
|
||||
);
|
||||
})}
|
||||
</BaseMenu.Popup>
|
||||
</BaseMenu.Positioner>
|
||||
</BaseMenu.Portal>
|
||||
</BaseMenu.Root>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
<IoEllipsisHorizontal />
|
||||
|
||||
@@ -126,10 +126,13 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
return (
|
||||
<MilestoneRow
|
||||
key={key}
|
||||
entryId={entry.id}
|
||||
isPast={isPast}
|
||||
parentBgColour={parentBgColour}
|
||||
parentId={entry.parent}
|
||||
rowBgColour={rowBgColour}
|
||||
rowId={row.id}
|
||||
rowIndex={index}
|
||||
table={table}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
<IoEllipsisHorizontal />
|
||||
|
||||
+26
-3
@@ -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<OntimeEntry>;
|
||||
}
|
||||
|
||||
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<AppMode>({
|
||||
key: sessionKeys.cuesheetMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
||||
|
||||
return (
|
||||
<tr
|
||||
@@ -36,7 +50,16 @@ export default function MilestoneRow({ isPast, parentBgColour, rowBgColour, rowI
|
||||
>
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
<IconButton aria-label='Options' variant='subtle-white' size='small' onClick={() => undefined}>
|
||||
<IconButton
|
||||
aria-label='Options'
|
||||
variant='subtle-white'
|
||||
size='small'
|
||||
onClick={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const yPos = 8 + rect.y + rect.height / 2;
|
||||
openMenu({ x: rect.x, y: yPos }, entryId, SupportedEntry.Milestone, rowIndex, parentId);
|
||||
}}
|
||||
>
|
||||
<IoEllipsisHorizontal />
|
||||
</IconButton>
|
||||
</td>
|
||||
|
||||
+53
-47
@@ -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 (
|
||||
<Portal>
|
||||
{isOpen && (
|
||||
<Menu isOpen size='sm' onClose={closeMenu} isLazy variant='ontime-on-dark'>
|
||||
<MenuButton
|
||||
position='absolute'
|
||||
left={position.x}
|
||||
top={position.y}
|
||||
pointerEvents='none'
|
||||
aria-hidden
|
||||
w={1}
|
||||
h={1}
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<IoOptions />} onClick={() => showModal(entryId)}>
|
||||
Edit ...
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={<IoAdd />}
|
||||
onClick={() => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId })}
|
||||
>
|
||||
Add event above
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<IoAdd />}
|
||||
onClick={() => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId })}
|
||||
>
|
||||
Add event below
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoDuplicateOutline />} onClick={() => clone(entryId)}>
|
||||
Clone event
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem isDisabled={entryIndex < 1} icon={<IoArrowUp />} onClick={() => move(entryId, 'up')}>
|
||||
Move up
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoArrowDown />} onClick={() => move(entryId, 'down')}>
|
||||
Move down
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoTrash />} onClick={() => deleteEntry([entryId])}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Portal>
|
||||
<Menu
|
||||
isOpen
|
||||
onClose={closeMenu}
|
||||
items={[
|
||||
{ type: 'item', label: 'Edit...', onClick: () => showModal(entryId), icon: <IoOptions /> },
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Add event above',
|
||||
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId }),
|
||||
icon: <IoAdd />,
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Add event below',
|
||||
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId }),
|
||||
icon: <IoAdd />,
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Clone event',
|
||||
onClick: () => clone(entryId),
|
||||
icon: <IoDuplicateOutline />,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Move up',
|
||||
onClick: () => move(entryId, 'up'),
|
||||
icon: <IoArrowUp />,
|
||||
disabled: entryIndex < 1,
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Move down',
|
||||
onClick: () => move(entryId, 'down'),
|
||||
icon: <IoArrowDown />,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Delete',
|
||||
onClick: () => deleteEntry([entryId]),
|
||||
icon: <IoTrash />,
|
||||
},
|
||||
]}
|
||||
position={position}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+18
-4
@@ -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<CuesheetTableMenuStore>((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 }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user