refactor: entry actions in cuesheet

fix: insert entry before
fix: avoid double submit on enter
fix: move events in the rundown
fix: clone groups
This commit is contained in:
Carlos Valente
2025-06-30 07:16:08 +02:00
committed by Carlos Valente
parent 8ad260d28a
commit 24a3823d3b
17 changed files with 592 additions and 301 deletions
@@ -1,10 +1,10 @@
import { memo, useCallback, useMemo, useRef } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTableNav } from '@table-nav/react';
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { OntimeEntry, TimeField } from 'ontime-types';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import useFollowComponent from '../../../common/hooks/useFollowComponent';
import { useFollowSelected } from '../../../common/hooks/useFollowComponent';
import { usePersistedCuesheetOptions } from '../cuesheet.options';
import CuesheetBody from './cuesheet-table-elements/CuesheetBody';
@@ -26,9 +26,7 @@ export default function CuesheetTable({ data, columns }: CuesheetTableProps) {
const showDelayedTimes = usePersistedCuesheetOptions((state) => state.showDelayedTimes);
const hideTableSeconds = usePersistedCuesheetOptions((state) => state.hideTableSeconds);
const selectedRef = useRef<HTMLTableRowElement | null>(null);
const tableContainerRef = useRef<HTMLDivElement | null>(null);
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followPlayback });
const { selectedRef, scrollRef } = useFollowSelected(followPlayback);
const { listeners } = useTableNav();
@@ -106,12 +104,13 @@ export default function CuesheetTable({ data, columns }: CuesheetTableProps) {
const headers = table.getFlatHeaders();
const colSizes: { [key: string]: number } = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i]!;
const header = headers[i];
if (!header) continue;
colSizes[`--header-${header.id}-size`] = header.getSize();
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
}
return colSizes;
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
}, [table]);
return (
<>
@@ -121,7 +120,7 @@ export default function CuesheetTable({ data, columns }: CuesheetTableProps) {
handleResetReordering={resetColumnOrder}
handleClearToggles={setAllVisible}
/>
<div ref={tableContainerRef} className={style.cuesheetContainer}>
<div className={style.cuesheetContainer} ref={scrollRef}>
<table className={style.cuesheet} id='cuesheet' style={{ ...columnSizeVars }} {...listeners}>
<CuesheetHeader headerGroups={headerGroups} />
{table.getState().columnSizingInfo.isResizingColumn ? (
@@ -40,7 +40,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);
openMenu({ x: rect.x, y: yPos }, blockId, rowIndex, null);
}}
>
<IoEllipsisHorizontal />
@@ -1,4 +1,4 @@
import { MutableRefObject, useMemo } from 'react';
import { RefObject, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { RowModel, Table } from '@tanstack/react-table';
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeBlock, OntimeEntry, Rundown } from 'ontime-types';
@@ -17,7 +17,7 @@ import { useVisibleRowsStore } from './visibleRowsStore';
interface CuesheetBodyProps {
rowModel: RowModel<OntimeEntry>;
selectedRef: MutableRefObject<HTMLTableRowElement | null>;
selectedRef: RefObject<HTMLTableRowElement>;
table: Table<OntimeEntry>;
}
@@ -106,7 +106,6 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
const isSelected = key === selectedEventId;
const columnHash = getColumnHash();
if (isPast && hidePast) {
return null;
}
@@ -129,14 +128,13 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
let firstAfterBlock = false;
if (entry.parent) {
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
const parentEntry = rundown?.entries[entry.parent];
parentBgColour = (parentEntry as OntimeBlock).colour;
const parentEntry = rundown?.entries[entry.parent] as OntimeBlock | undefined;
parentBgColour = parentEntry?.colour;
hadBlock = true;
} else if (hadBlock) {
firstAfterBlock = true;
hadBlock = false;
}
return (
<EventRow
@@ -1,4 +1,4 @@
import { memo, MutableRefObject, useLayoutEffect, useRef } from 'react';
import { MutableRefObject, useLayoutEffect, useRef } from 'react';
import { IoEllipsisHorizontal } from 'react-icons/io5';
import { flexRender, Table } from '@tanstack/react-table';
import { OntimeEntry, OntimeEvent, RGBColour } from 'ontime-types';
@@ -31,21 +31,7 @@ interface EventRowProps {
firstAfterBlock: boolean;
}
export default memo(EventRow, (prevProps, nextProps) => {
return (
prevProps.rowId === nextProps.rowId &&
prevProps.event.revision === nextProps.event.revision &&
prevProps.eventIndex === nextProps.eventIndex &&
prevProps.rowIndex === nextProps.rowIndex &&
prevProps.isPast === nextProps.isPast &&
prevProps.selectedRef === nextProps.selectedRef &&
prevProps.rowBgColour === nextProps.rowBgColour &&
prevProps.parentBgColour === nextProps.parentBgColour &&
prevProps.columnHash === nextProps.columnHash
);
});
function EventRow({
export default function EventRow({
rowId,
event,
eventIndex,
@@ -87,7 +73,12 @@ function EventRow({
return (
<tr
id={rowId}
className={cx([style.eventRow, event.skip && style.skip, firstAfterBlock && style.firstAfterBlock, Boolean(parentBgColour) && style.hasParent])}
className={cx([
style.eventRow,
event.skip && style.skip,
firstAfterBlock && style.firstAfterBlock,
Boolean(parentBgColour) && style.hasParent,
])}
style={{
opacity: `${isPast ? '0.2' : '1'}`,
'--user-bg': parentBgColour ?? 'transparent',
@@ -103,7 +94,7 @@ 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);
openMenu({ x: rect.x, y: yPos }, event.id, rowIndex, event.parent);
}}
>
<IoEllipsisHorizontal />
@@ -1,10 +1,9 @@
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 { isOntimeEvent, SupportedEntry } from 'ontime-types';
import { SupportedEntry } from 'ontime-types';
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
import { cloneEvent } from '../../../../common/utils/clone';
import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal';
import { useCuesheetTableMenu } from './useCuesheetTableMenu';
@@ -12,28 +11,10 @@ import { useCuesheetTableMenu } from './useCuesheetTableMenu';
export default memo(CuesheetTableMenu);
function CuesheetTableMenu() {
const { isOpen, eventId, entryIndex, position, closeMenu } = useCuesheetTableMenu();
const { addEntry, getEntryById, move, deleteEntry } = useEntryActions();
const { isOpen, entryId, entryIndex, parentId, position, closeMenu } = useCuesheetTableMenu();
const { addEntry, clone, deleteEntry, move } = useEntryActions();
const showModal = useCuesheetEditModal((state) => state.setEditableEntry);
const handleCloneEvent = () => {
if (!eventId) {
return;
}
const currentEvent = getEntryById(eventId);
if (!currentEvent || !isOntimeEvent(currentEvent)) {
return;
}
const newEvent = cloneEvent(currentEvent);
try {
addEntry(newEvent, { after: eventId });
} catch (_error) {
// we do not handle errors here
}
};
return (
<Portal>
{isOpen && (
@@ -48,27 +29,33 @@ function CuesheetTableMenu() {
h={1}
/>
<MenuList>
<MenuItem icon={<IoOptions />} onClick={() => showModal(eventId)}>
<MenuItem icon={<IoOptions />} onClick={() => showModal(entryId)}>
Edit ...
</MenuItem>
<MenuDivider />
<MenuItem icon={<IoAdd />} onClick={() => addEntry({ type: SupportedEntry.Event }, { before: eventId })}>
<MenuItem
icon={<IoAdd />}
onClick={() => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId })}
>
Add event above
</MenuItem>
<MenuItem icon={<IoAdd />} onClick={() => addEntry({ type: SupportedEntry.Event }, { after: eventId })}>
<MenuItem
icon={<IoAdd />}
onClick={() => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId })}
>
Add event below
</MenuItem>
<MenuItem icon={<IoDuplicateOutline />} onClick={handleCloneEvent}>
<MenuItem icon={<IoDuplicateOutline />} onClick={() => clone(entryId)}>
Clone event
</MenuItem>
<MenuDivider />
<MenuItem isDisabled={entryIndex < 1} icon={<IoArrowUp />} onClick={() => move(eventId, 'up')}>
<MenuItem isDisabled={entryIndex < 1} icon={<IoArrowUp />} onClick={() => move(entryId, 'up')}>
Move up
</MenuItem>
<MenuItem icon={<IoArrowDown />} onClick={() => move(eventId, 'down')}>
<MenuItem icon={<IoArrowDown />} onClick={() => move(entryId, 'down')}>
Move down
</MenuItem>
<MenuItem icon={<IoTrash />} onClick={() => deleteEntry([eventId])}>
<MenuItem icon={<IoTrash />} onClick={() => deleteEntry([entryId])}>
Delete
</MenuItem>
</MenuList>
@@ -1,31 +1,35 @@
import { EntryId } from 'ontime-types';
import { create } from 'zustand';
type Anchor = { x: number; y: number };
type OpenMenu = {
isOpen: true;
eventId: string;
entryId: EntryId;
entryIndex: number;
parentId: EntryId | null;
};
type ClosedMenu = {
isOpen: false;
eventId: null;
entryId: null;
entryIndex: null;
parentId: null;
};
type CuesheetTableMenuStore = (OpenMenu | ClosedMenu) & {
position: Anchor;
openMenu: (position: Anchor, eventId: string, entryIndex: number) => void;
openMenu: (position: Anchor, entryId: EntryId, entryIndex: number, parentId: EntryId | null) => void;
closeMenu: () => void;
};
export const useCuesheetTableMenu = create<CuesheetTableMenuStore>((set) => ({
isOpen: false,
eventId: null,
entryId: null,
entryIndex: null,
parentId: null,
position: { x: 0, y: 0 },
openMenu: (position: Anchor, eventId: string, entryIndex: number) =>
set({ isOpen: true, position, eventId, entryIndex }),
openMenu: (position: Anchor, entryId: EntryId, entryIndex: number, parentId: EntryId | null) =>
set({ isOpen: true, position, entryId, entryIndex, parentId }),
closeMenu: () => set({ isOpen: false }),
}));