Merge remote-tracking branch 'origin/master' into claude/cuesheet-scroll-perf-7nogws

This commit is contained in:
Claude
2026-06-20 07:06:18 +00:00
5 changed files with 62 additions and 14 deletions
@@ -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 {
@@ -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 (
<tbody className={style.emptyContainer}>
<tr>
<td colSpan={99} className={style.emptyCell}>
<EmptyImage className={style.empty} />
{text && <span className={style.text}>{text}</span>}
<Empty injectedStyles={{ marginTop: '5vh' }} />
<span className={style.text}>{text}</span>
{handleAddNew && (
<div className={style.inline}>
<Button onClick={() => handleAddNew(SupportedEntry.Event)} variant='primary' size='large'>
<IoAdd />
Create Event
</Button>
</div>
)}
</td>
</tr>
</tbody>
@@ -157,7 +157,7 @@ export default function ManageRundowns() {
</td>
<td>
<DropdownMenu
render={<IconButton variant='ghosted-white' />}
render={<IconButton variant='ghosted-white' data-testId='rundown_menu' />}
items={[
{
type: 'item',
@@ -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<typeof useTableNav>['listeners'];
rows: ReturnType<Table<ExtendedEntry>['getRowModel']>['rows'];
table: Table<ExtendedEntry>;
handleAddNew?: (type: SupportedEntry) => void;
}
const EmptyPlaceholder = memo(function EmptyPlaceholder() {
return <EmptyTableBody text='No data in rundown' />;
const EmptyPlaceholder = memo(function EmptyPlaceholder({
context,
}: TableProps & ContextProp<CuesheetVirtuosoContext>) {
return <EmptyTableBody handleAddNew={context.handleAddNew} />;
});
const CuesheetTableElement = memo(function CuesheetTableElement({
+24
View File
@@ -108,3 +108,27 @@ test('cuesheet datagrid keeps keyboard focus flow while editing text cells', asy
await expect(cueCell).toContainText(cueBeforeCancel);
await expect(cueCell).toBeFocused();
});
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();
});