From b946b245c2e095f6770e0a9eb52c2768a980cf1e Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Wed, 17 Jun 2026 22:31:13 +0200 Subject: [PATCH] feat: create the first event from the cuesheet (#2105) * feat: create the first event from the cuesheet * chore: cleanup css * test: cuesheet beckground edit from empty state * feat: only show +event button when user is in edit mode and have full write perms --- .../state/EmptyTableBody.module.scss | 9 ++++--- .../components/state/EmptyTableBody.tsx | 25 +++++++++++++++---- .../panel/manage-panel/ManageRundowns.tsx | 2 +- .../cuesheet/cuesheet-table/CuesheetTable.tsx | 16 ++++++++---- e2e/tests/features/202-cuesheet.spec.ts | 24 ++++++++++++++++++ 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/apps/client/src/common/components/state/EmptyTableBody.module.scss b/apps/client/src/common/components/state/EmptyTableBody.module.scss index 678dd083f..b2288ffef 100644 --- a/apps/client/src/common/components/state/EmptyTableBody.module.scss +++ b/apps/client/src/common/components/state/EmptyTableBody.module.scss @@ -8,9 +8,12 @@ padding-top: 10vh; } - .empty { - width: 100%; - opacity: 0.8; + .inline { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-top: 1em; } .text { diff --git a/apps/client/src/common/components/state/EmptyTableBody.tsx b/apps/client/src/common/components/state/EmptyTableBody.tsx index 98a771d4f..0edd2a22c 100644 --- a/apps/client/src/common/components/state/EmptyTableBody.tsx +++ b/apps/client/src/common/components/state/EmptyTableBody.tsx @@ -1,18 +1,33 @@ -import EmptyImage from '../../../assets/images/empty.svg?react'; +import { SupportedEntry } from 'ontime-types'; +import { IoAdd } from 'react-icons/io5'; + +import { useTranslation } from '../../../translation/TranslationProvider'; +import Button from '../buttons/Button'; +import Empty from './Empty'; import style from './EmptyTableBody.module.scss'; interface EmptyTableBodyProps { - text: string; + handleAddNew?: (type: SupportedEntry) => void; } -export default function EmptyTableBody({ text }: EmptyTableBodyProps) { +export default function EmptyTableBody({ handleAddNew }: EmptyTableBodyProps) { + const { getLocalizedString } = useTranslation(); + const text = getLocalizedString('common.no_data'); return ( - - {text && {text}} + + {text} + {handleAddNew && ( +
+ +
+ )} diff --git a/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx b/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx index 3018cd916..0bc3e6c9b 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx +++ b/apps/client/src/features/app-settings/panel/manage-panel/ManageRundowns.tsx @@ -157,7 +157,7 @@ export default function ManageRundowns() { } + render={} items={[ { type: 'item', diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index 89b155c68..32c3f60ac 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -1,6 +1,6 @@ import { useTableNav } from '@table-nav/react'; import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table'; -import { OntimeEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types'; +import { OntimeEntry, SupportedEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types'; import { ComponentProps, ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react'; import { ContextProp, @@ -20,6 +20,7 @@ import { usePersistedRundownOptions } from '../../../features/rundown/rundown.op import { useEventSelection } from '../../../features/rundown/useEventSelection'; import { AppMode } from '../../../ontimeConfig'; import { usePersistedCuesheetOptions } from '../cuesheet.options'; +import { useCuesheetPermissions } from '../useTablePermissions'; import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader'; import DelayRow from './cuesheet-table-elements/DelayRow'; import EventRow from './cuesheet-table-elements/EventRow'; @@ -62,7 +63,8 @@ export default function CuesheetTable({ insertElement, }: CuesheetTableProps) { const { flatRundown, status, selectedEventId } = source; - const { updateEntry, updateTimer } = useEntryActionsContext(); + const { updateEntry, updateTimer, addEntry } = useEntryActionsContext(); + const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit; const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions; const optionsStore = useOptions(); @@ -202,8 +204,9 @@ export default function CuesheetTable({ listeners, rows, table, + handleAddNew: canCreateEntries ? (type: SupportedEntry) => addEntry({ type }) : undefined, }), - [columnSizeVars, cursor, listeners, rows, table], + [columnSizeVars, cursor, listeners, rows, table, addEntry, canCreateEntries], ); const computeItemKey = useCallback((_: number, item: ExtendedEntry) => item.id, []); @@ -273,10 +276,13 @@ interface CuesheetVirtuosoContext { listeners: ReturnType['listeners']; rows: ReturnType['getRowModel']>['rows']; table: Table; + handleAddNew?: (type: SupportedEntry) => void; } -const EmptyPlaceholder = memo(function EmptyPlaceholder() { - return ; +const EmptyPlaceholder = memo(function EmptyPlaceholder({ + context, +}: TableProps & ContextProp) { + return ; }); const CuesheetTableElement = memo(function CuesheetTableElement({ diff --git a/e2e/tests/features/202-cuesheet.spec.ts b/e2e/tests/features/202-cuesheet.spec.ts index ea1b76c51..46d118b87 100644 --- a/e2e/tests/features/202-cuesheet.spec.ts +++ b/e2e/tests/features/202-cuesheet.spec.ts @@ -100,3 +100,27 @@ test('cuesheet datagrid keeps keyboard focus flow while editing text cells', asy await expect(cueEditor).not.toBeFocused(); await expect(cueEditor).toHaveValue(cueBeforeCancel); }); + +test('cuesheet background edit from empty state', async ({ page }) => { + // create an empty rundown + await page.goto('/editor'); + await page.getByRole('button', { name: 'Toggle settings' }).click(); + await page.getByRole('button', { name: 'Manage rundowns' }).click(); + await page.getByRole('button', { name: 'New' }).nth(1).click(); + const emptyName = `empty-${Date.now()}`; + await page.getByRole('textbox', { name: 'Rundown title' }).fill(emptyName); + await page.getByRole('button', { name: 'Create rundown' }).click(); + + // edit it in the cuesheet + await page.getByRole('row', { name: '0 empty-' }).getByTestId('rundown_menu').click(); + await page.getByText('Edit in cuesheet').click(); + + // expect to see and empty screen + await expect(page.getByRole('button', { name: 'Create Event' })).toBeVisible(); + + // create 1 event + await page.getByRole('button', { name: 'Create Event' }).click(); + + // and expect to find it + await expect(page.getByTestId('cuesheet-event')).toBeVisible(); +});