fix: rundown shortcuts

This commit is contained in:
Carlos Valente
2025-07-02 05:42:17 +02:00
committed by Carlos Valente
parent d06b5af538
commit 2e9ee698e9
10 changed files with 119 additions and 57 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@fontsource/open-sans": "^5.0.28",
"@mantine/hooks": "^7.17.2",
"@mantine/hooks": "^8.1.2",
"@sentry/react": "^8.43.0",
"@table-nav/react": "^0.0.7",
"@tanstack/react-query": "^5.62.7",
+15 -3
View File
@@ -247,13 +247,25 @@ export default function Rundown({ data }: RundownProps) {
['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
['alt + E', () => insertAtId({ type: SupportedEntry.Event }, cursor), { preventDefault: true }],
[
'alt + E',
() => insertAtId({ type: SupportedEntry.Event }, cursor),
{ preventDefault: true, usePhysicalKeys: true },
],
['alt + shift + E', () => insertAtId({ type: SupportedEntry.Event }, cursor, true), { preventDefault: true }],
['alt + B', () => insertAtId({ type: SupportedEntry.Block }, cursor), { preventDefault: true }],
[
'alt + B',
() => insertAtId({ type: SupportedEntry.Block }, cursor),
{ preventDefault: true, usePhysicalKeys: true },
],
['alt + shift + B', () => insertAtId({ type: SupportedEntry.Block }, cursor, true), { preventDefault: true }],
['alt + D', () => insertAtId({ type: SupportedEntry.Delay }, cursor), { preventDefault: true }],
[
'alt + D',
() => insertAtId({ type: SupportedEntry.Delay }, cursor),
{ preventDefault: true, usePhysicalKeys: true },
],
['alt + shift + D', () => insertAtId({ type: SupportedEntry.Delay }, cursor, true), { preventDefault: true }],
['mod + C', () => setEntryCopyId(cursor)],
@@ -105,6 +105,7 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
...(binderColours ? { '--user-bg': binderColours.backgroundColor } : {}),
...dragStyle,
}}
data-testid='rundown-block'
>
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
<span
@@ -58,7 +58,7 @@ export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
const blockClasses = cx([style.delay, hasCursor ? style.hasCursor : null]);
return (
<div className={blockClasses} ref={setNodeRef} style={dragStyle}>
<div className={blockClasses} ref={setNodeRef} style={dragStyle} data-testid='rundown-delay'>
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<IoReorderTwo />
</span>
@@ -12,6 +12,7 @@ import {
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { EndAction, EntryId, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
import { isPlaybackActive } from 'ontime-utils';
import { useContextMenu } from '../../../common/hooks/useContextMenu';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
@@ -248,6 +249,8 @@ export default function RundownEvent({
setSelectedEvents({ id: eventId, index, selectMode: editMode });
};
const isPlaying = playback ? isPlaybackActive(playback) : false;
return (
<div
className={blockClasses}
@@ -255,7 +258,8 @@ export default function RundownEvent({
style={dragStyle}
onClick={handleFocusClick}
onContextMenu={onContextMenu}
id='event-block'
data-testid='rundown-event'
{...(isPlaying ? { 'data-running': true } : {})}
>
<RundownIndicators timeStart={timeStart} delay={delay} gap={gap} isNextDay={isNextDay} />
@@ -6,22 +6,37 @@ test('CRUD operations on the rundown', async ({ page }) => {
await page.getByRole('button', { name: 'Edit' }).click();
await page.getByRole('button', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await expect(page.getByTestId('rundown-delay')).toHaveCount(0);
await expect(page.getByTestId('rundown-block')).toHaveCount(0);
// create event from the rundown empty button
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-delay')).toHaveCount(0);
await expect(page.getByTestId('rundown-block')).toHaveCount(0);
// create blocks using the quick add buttons
await page.getByRole('button', { name: 'Block' }).nth(1).click();
await page.getByRole('button', { name: 'Delay' }).nth(1).click();
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await expect(page.getByTestId('rundown-delay')).toHaveCount(1);
await expect(page.getByTestId('rundown-block')).toHaveCount(1);
// test quick add options - star2+5-t is last end
await page.getByTestId('entry-2').getByTestId('time-input-duration').fill('20m');
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain('00:30:00');
await expect(page.getByTestId('rundown-event')).toHaveCount(3);
await expect(page.getByTestId('rundown-delay')).toHaveCount(1);
await expect(page.getByTestId('rundown-block')).toHaveCount(1);
// test quick add options
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(4);
await expect(page.getByTestId('rundown-delay')).toHaveCount(1);
await expect(page.getByTestId('rundown-block')).toHaveCount(1);
await expect(page.getByTestId('entry-4').locator('#block-status')).toHaveAttribute('data-timerType', 'count-down');
});
@@ -26,7 +26,7 @@ test('Copy-paste', async ({ page }) => {
// assert
await expect(page.getByTestId('entry-2')).toBeVisible();
await expect(page.getByTestId('entry-2').getByTestId('block__title')).toHaveValue('test');
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('5');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5');
// copy paste above
await page.locator('div').filter({ hasText: /^5$/ }).click();
@@ -36,7 +36,7 @@ test('Copy-paste', async ({ page }) => {
// assert
await expect(page.getByTestId('entry-2')).toBeVisible();
await expect(page.getByTestId('entry-2').getByTestId('block__title')).toHaveValue('test');
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('4.1');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('4.1');
});
test('Move', async ({ page }) => {
@@ -52,15 +52,19 @@ test('Move', async ({ page }) => {
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
// copy move down
await page.getByTestId('entry-1').locator('#event-block').getByText('1').click();
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+Control+ArrowDown');
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('1');
await page.getByTestId('entry-1').getByTestId('rundown-event').getByText('1').click();
await page
.getByTestId('entry-1')
.getByTestId('rundown-event')
.filter({ hasText: '1' })
.press('Alt+Control+ArrowDown');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('1');
// copy move up
await page.getByTestId('entry-3').locator('#event-block').getByText('3').click();
await page.getByTestId('entry-3').locator('#event-block div').filter({ hasText: '3' }).press('Alt+Control+ArrowUp');
await page.getByTestId('entry-3').getByTestId('rundown-event').getByText('3').click();
await page.getByTestId('entry-3').getByTestId('rundown-event').filter({ hasText: '3' }).press('Alt+Control+ArrowUp');
await page.getByTestId('entry-2').locator('div').filter({ hasText: /^3$/ }).press('Alt+Control+ArrowUp');
await expect(page.getByTestId('entry-1').locator('#event-block')).toContainText('3');
await expect(page.getByTestId('entry-1').getByTestId('rundown-event')).toContainText('3');
});
test('Add block', async ({ page }) => {
@@ -69,22 +73,27 @@ test('Add block', async ({ page }) => {
// clear rundown
await page.getByRole('button', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await expect(page.getByTestId('rundown-block')).toHaveCount(0);
// create events
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-block')).toHaveCount(0);
await page.getByPlaceholder(/event title/i).fill('test');
await page.getByTestId('entry-1').click();
// add block below
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+B');
await page.getByPlaceholder(/block title/i).fill('block below');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+B');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-block')).toHaveCount(1);
await page.getByTestId('rundown-block').getByTestId('block__title').fill('block below');
// add block above
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+Shift+B');
await page
.getByPlaceholder(/block title/i)
.first()
.fill('block above');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Shift+B');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-block')).toHaveCount(2);
await page.getByTestId('block__title').first().fill('block above');
await expect(page.getByTestId(/block__title/i).first()).toHaveValue('block above');
await expect(page.getByTestId(/block__title/i).nth(2)).toHaveValue('block below');
@@ -97,18 +106,26 @@ test('Add delay', async ({ page }) => {
// clear rundown
await page.getByRole('button', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await expect(page.getByTestId('rundown-delay')).toHaveCount(0);
//create events
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-delay')).toHaveCount(0);
await page.getByTestId('entry-1').click();
await page.getByTestId('block__title').press('Escape');
//add delay below
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+D');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+D');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-delay')).toHaveCount(1);
await expect(page.getByTestId('delay-input')).toBeVisible();
//add delay above
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+Shift+D');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Shift+D');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-delay')).toHaveCount(2);
await expect(page.getByTestId('entry-0').getByTestId('delay-input')).toBeVisible();
});
@@ -118,19 +135,23 @@ test('Add event', async ({ page }) => {
// clear rundown
await page.getByRole('button', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
//create events
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByTestId('entry-1').click();
await page.getByTestId('block__title').press('Escape');
//add event below
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+E');
await expect(page.getByTestId('entry-2').locator('#event-block').getByText('2')).toBeVisible();
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+E');
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await expect(page.getByTestId('entry-2').getByTestId('rundown-event').getByText('2')).toBeVisible();
//add event above
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+Shift+E');
await expect(page.getByTestId('entry-1').locator('#event-block')).toContainText('0.1');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Shift+E');
await expect(page.getByTestId('rundown-event')).toHaveCount(3);
await expect(page.getByTestId('entry-1').getByTestId('rundown-event')).toContainText('0.1');
});
test('Delete event', async ({ page }) => {
@@ -140,12 +161,16 @@ test('Delete event', async ({ page }) => {
await page.goto('http://localhost:4001/rundown');
await page.getByRole('button', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
//create event
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
//delete event
await page.locator('#event-block div').filter({ hasText: '1' }).click();
await page.getByTestId('entry-1').locator('#event-block div').filter({ hasText: '1' }).press('Alt+Backspace');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).click();
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Backspace');
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Create Event' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Create Block' })).toBeVisible();
});
+18 -18
View File
@@ -16,25 +16,25 @@ test('time until absolute', async ({ page }) => {
await expect(page.getByTestId('offset')).not.toContainText('00:00:00'); // This might be a bad test requires that the test is not run at 0h
await page.getByLabel('Pause event').click();
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('9m');
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('19m');
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('29m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m');
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('5h59m');
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('6h9m');
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('6h19m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m');
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('30s');
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('10m');
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('20m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
});
test('time until relative', async ({ page }) => {
@@ -53,23 +53,23 @@ test('time until relative', async ({ page }) => {
await expect(page.getByTestId('offset')).toContainText('00:00:00'); // This might be a bad test as it ruires the evaluation to happen within 1s
await page.getByLabel('Pause event').click();
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('9m');
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('19m');
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('29m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m');
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('5h59m');
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('6h9m');
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('6h19m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m');
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').click();
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('30s');
await expect(page.getByTestId('entry-3').locator('#event-block')).toContainText('10m');
await expect(page.getByTestId('entry-4').locator('#event-block')).toContainText('20m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
});
@@ -14,14 +14,19 @@ test('Rearrange while playing', async ({ page }) => {
// start event 2
await page.getByTestId('entry-2').getByRole('button', { name: 'Start event' }).click();
await expect(page.getByTestId('entry-2').locator('#event-block')).toHaveCSS('background-color', 'rgb(8, 122, 39)');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toHaveAttribute('data-running');
// move event 2 up
await page.getByTestId('entry-2').locator('#event-block').getByText('2').click();
await page.getByTestId('entry-2').locator('#event-block div').filter({ hasText: '2' }).press('Alt+Control+ArrowUp');
await page.getByTestId('entry-2').getByTestId('rundown-event').locator('div').filter({ hasText: '2' }).click();
await page
.getByTestId('entry-2')
.getByTestId('rundown-event')
.locator('div')
.filter({ hasText: '2' })
.press('Alt+Control+ArrowUp');
// event CUE1 should new be entry 2
await expect(page.getByTestId('entry-2').locator('#event-block')).toContainText('1');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('1');
// but entry 1 should be the one playing (it will be unlinked as it will be the first event)
await expect(page.getByTestId('entry-1').locator('#event-block')).toHaveCSS('background-color', 'rgb(8, 122, 39)');
await expect(page.getByTestId('entry-1').getByTestId('rundown-event')).toHaveAttribute('data-running');
});
+5 -5
View File
@@ -114,8 +114,8 @@ importers:
specifier: ^5.0.28
version: 5.2.6
'@mantine/hooks':
specifier: ^7.17.2
version: 7.17.8(react@18.3.1)
specifier: ^8.1.2
version: 8.1.2(react@18.3.1)
'@sentry/react':
specifier: ^8.43.0
version: 8.55.0(react@18.3.1)
@@ -1315,8 +1315,8 @@ packages:
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
engines: {node: '>= 10.0.0'}
'@mantine/hooks@7.17.8':
resolution: {integrity: sha512-96qygbkTjRhdkzd5HDU8fMziemN/h758/EwrFu7TlWrEP10Vw076u+Ap/sG6OT4RGPZYYoHrTlT+mkCZblWHuw==}
'@mantine/hooks@8.1.2':
resolution: {integrity: sha512-BrriTsiazqZ2fLuL7UDasNTQJSaoJ7mN2qYVkdsiYI158lxJdUaFWHhd6BDyzK+W6thvBx2D+R/hh1rsWWefdQ==}
peerDependencies:
react: ^18.x || ^19.x
@@ -6455,7 +6455,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mantine/hooks@7.17.8(react@18.3.1)':
'@mantine/hooks@8.1.2(react@18.3.1)':
dependencies:
react: 18.3.1