diff --git a/apps/client/src/features/rundown/block-block/BlockBlock.module.scss b/apps/client/src/features/rundown/block-block/BlockBlock.module.scss index f01af1477..6be287a51 100644 --- a/apps/client/src/features/rundown/block-block/BlockBlock.module.scss +++ b/apps/client/src/features/rundown/block-block/BlockBlock.module.scss @@ -6,11 +6,12 @@ background-color: $block-bg2; + min-width: 34rem; display: grid; - grid-template-columns: 32px 1fr auto; + grid-template-columns: 2rem 1fr auto; align-items: center; height: $secondary-block-height; - gap: 8px; + gap: 0.5rem; &.hasCursor { outline: 1px solid $block-cursor-color; diff --git a/apps/client/src/features/rundown/block-block/BlockBlock.tsx b/apps/client/src/features/rundown/block-block/BlockBlock.tsx index e4f5bce99..c1248664e 100644 --- a/apps/client/src/features/rundown/block-block/BlockBlock.tsx +++ b/apps/client/src/features/rundown/block-block/BlockBlock.tsx @@ -59,7 +59,7 @@ export default function BlockBlock(props: BlockBlockProps) { - + ); } diff --git a/apps/client/src/features/rundown/delay-block/DelayBlock.module.scss b/apps/client/src/features/rundown/delay-block/DelayBlock.module.scss index 1365ff7ae..f0c218a03 100644 --- a/apps/client/src/features/rundown/delay-block/DelayBlock.module.scss +++ b/apps/client/src/features/rundown/delay-block/DelayBlock.module.scss @@ -6,12 +6,13 @@ background-color: $block-bg2; + min-width: 34rem; display: grid; - grid-template-columns: 32px 1fr auto; + grid-template-columns: 2rem 1fr auto; grid-template-areas: 'drag inpt btns'; align-items: center; height: $secondary-block-height; - gap: 8px; + gap: 0.5rem; &.hasCursor { outline: 1px solid $block-cursor-color; diff --git a/apps/client/src/features/rundown/delay-block/DelayBlock.tsx b/apps/client/src/features/rundown/delay-block/DelayBlock.tsx index 384310221..c86fb149b 100644 --- a/apps/client/src/features/rundown/delay-block/DelayBlock.tsx +++ b/apps/client/src/features/rundown/delay-block/DelayBlock.tsx @@ -79,7 +79,7 @@ export default function DelayBlock(props: DelayBlockProps) { - + ); diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index d224373b8..3ca8e4749 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -144,7 +144,10 @@ const EventBlockInner = (props: EventBlockInnerProps) => { - + @@ -163,7 +166,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => { color={isOpen ? 'white' : '#f6f6f6'} isDisabled={disableEdit} /> - + ); diff --git a/apps/client/src/features/rundown/event-block/composite/BlockActionMenu.tsx b/apps/client/src/features/rundown/event-block/composite/BlockActionMenu.tsx index 6af87948b..4f0b2470e 100644 --- a/apps/client/src/features/rundown/event-block/composite/BlockActionMenu.tsx +++ b/apps/client/src/features/rundown/event-block/composite/BlockActionMenu.tsx @@ -11,9 +11,6 @@ import { tooltipDelayMid } from '../../../../ontimeConfig'; import { EventItemActions } from '../../RundownEntry'; interface BlockActionMenuProps { - showAdd?: boolean; - showDelay?: boolean; - showBlock?: boolean; enableDelete?: boolean; showClone?: boolean; actionHandler: (action: EventItemActions, payload?: any) => void; @@ -21,7 +18,7 @@ interface BlockActionMenuProps { } export default function BlockActionMenu(props: BlockActionMenuProps) { - const { showAdd, showDelay, showBlock, enableDelete, showClone, actionHandler, className } = props; + const { enableDelete, showClone, actionHandler, className } = props; const handleAddEvent = useCallback(() => actionHandler('event'), [actionHandler]); const handleAddDelay = useCallback(() => actionHandler('delay'), [actionHandler]); @@ -43,17 +40,17 @@ export default function BlockActionMenu(props: BlockActionMenuProps) { /> - } onClick={handleAddEvent} isDisabled={!showAdd}> + } onClick={handleAddEvent}> Add Event after - } onClick={handleAddDelay} isDisabled={!showDelay}> + } onClick={handleAddDelay}> Add Delay after - } onClick={handleAddBlock} isDisabled={!showBlock}> + } onClick={handleAddBlock}> Add Block after {showClone && ( - } onClick={handleClone} isDisabled={!showBlock}> + } onClick={handleClone}> Clone event )} diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx index 2551a3308..350640834 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx @@ -80,6 +80,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => { size='xs' variant='ontime-subtle-white' className={style.quickBtn} + data-testId='quick-add-event' > Event {showKbd && Alt + E} @@ -91,6 +92,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => { variant='ontime-subtle-white' disabled={disableAddDelay} className={style.quickBtn} + data-testId='quick-add-delay' > Delay {showKbd && Alt + D} @@ -102,6 +104,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => { variant='ontime-subtle-white' disabled={disableAddBlock} className={style.quickBtn} + data-testId='quick-add-block' > Block {showKbd && Alt + B} diff --git a/e2e/tests/features/204-editor-crud.spec.ts b/e2e/tests/features/204-editor-crud.spec.ts new file mode 100644 index 000000000..6f23230ef --- /dev/null +++ b/e2e/tests/features/204-editor-crud.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; + +test('CRUD operations on the rundown', async ({ page }) => { + await page.goto('http://localhost:4001/editor'); + + // clear rundown + await page.getByRole('button', { name: 'Run mode' }).click(); + await page.getByRole('button', { name: 'Event...' }).click(); + await page.getByRole('menuitem', { name: 'Delete all events' }).click(); + + // create event from the rundown empty button + await page.getByRole('button', { name: 'Create Event' }).click(); + + // create blocks using the quick add buttons + await page.getByRole('button', { name: 'Block' }).click(); + await page.getByRole('button', { name: 'Delay' }).click(); + await page.getByTestId('quick-add-event').click(); + + // test quick add options - start is last end + await page + .locator('div') + .filter({ hasText: /^22SEDEvent title$/ }) + .getByTestId('time-input-timeEnd') + .click(); + await page + .locator('div') + .filter({ hasText: /^22SEDEvent title$/ }) + .getByTestId('time-input-timeEnd') + .fill('20m'); + await page + .locator('div') + .filter({ hasText: /^22SEDEvent title$/ }) + .getByTestId('time-input-timeEnd') + .press('Enter'); + await page.getByText('Start time is last end').click(); + await page.getByTestId('quick-add-event').click(); + await expect( + await page + .locator('div') + .filter({ hasText: /^33SEDEvent title$/ }) + .getByTestId('time-input-timeStart') + .inputValue(), + ).toContain('00:20:00'); + + // test quick add options - event is public + await page.locator('label').filter({ hasText: 'Event is public' }).click(); + await page.getByTestId('quick-add-event').click(); + + await expect( + await page + .locator('div') + .filter({ hasText: /^44SEDEvent title$/ }) + .getByRole('img') + .nth(3), + ).toHaveAttribute('data-isPublic', 'true'); +}); diff --git a/packages/types/src/utils/guards.ts b/packages/types/src/utils/guards.ts index 11f132707..ca31381a5 100644 --- a/packages/types/src/utils/guards.ts +++ b/packages/types/src/utils/guards.ts @@ -1,14 +1,16 @@ import { OntimeRundownEntry } from '../definitions/core/Rundown.type.js'; import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from '../definitions/core/OntimeEvent.type.js'; -export function isOntimeEvent(event: OntimeRundownEntry | Partial): event is OntimeEvent { +type MaybeEvent = OntimeRundownEntry | null | undefined; + +export function isOntimeEvent(event: MaybeEvent): event is OntimeEvent { return event?.type === SupportedEvent.Event; } -export function isOntimeDelay(event: OntimeRundownEntry | Partial): event is OntimeDelay { +export function isOntimeDelay(event: MaybeEvent): event is OntimeDelay { return event?.type === SupportedEvent.Delay; } -export function isOntimeBlock(event: OntimeRundownEntry | Partial): event is OntimeBlock { +export function isOntimeBlock(event: MaybeEvent): event is OntimeBlock { return event?.type === SupportedEvent.Block; } diff --git a/packages/utils/src/cue-utils/cueUtils.test.ts b/packages/utils/src/cue-utils/cueUtils.test.ts index 8f26dec4a..c0a76bcd1 100644 --- a/packages/utils/src/cue-utils/cueUtils.test.ts +++ b/packages/utils/src/cue-utils/cueUtils.test.ts @@ -1,4 +1,4 @@ -import { OntimeRundown, SupportedEvent } from 'ontime-types'; +import { OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types'; import { getCueCandidate, getIncrement, sanitiseCue } from './cueUtils.js'; @@ -32,7 +32,7 @@ describe('getIncrement()', () => { }); }); -describe('findCueName()', () => { +describe('getCueCandidate()', () => { describe('in the beginning of the rundown', () => { it('names cue as 1 if next event does not collide', () => { const testRundown = [ @@ -89,6 +89,24 @@ describe('findCueName()', () => { expect(cue).toBe('Presenter1.1'); }); }); + describe('considers edge cases', () => { + it('previousEvent might not be a cue', () => { + const testRundown = [ + { id: '1', cue: '10', type: SupportedEvent.Event } as OntimeEvent, + { id: '2', type: SupportedEvent.Delay } as OntimeDelay, + ]; + const cue = getCueCandidate(testRundown, '2'); + expect(cue).toBe('11'); + }); + }); + it('there might not be events before', () => { + const testRundown = [ + { id: '1', type: SupportedEvent.Delay } as OntimeDelay, + { id: '2', type: SupportedEvent.Delay } as OntimeDelay, + ]; + const cue = getCueCandidate(testRundown, '2'); + expect(cue).toBe('1'); + }); }); describe('findCueName() with mixed events', () => { diff --git a/packages/utils/src/cue-utils/cueUtils.ts b/packages/utils/src/cue-utils/cueUtils.ts index 89b820a61..e700b77b4 100644 --- a/packages/utils/src/cue-utils/cueUtils.ts +++ b/packages/utils/src/cue-utils/cueUtils.ts @@ -1,6 +1,6 @@ -import { OntimeEvent, OntimeRundown } from 'ontime-types'; +import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types'; -import { getFirstEvent, getNextEvent } from '../rundown-utils/rundownUtils.js'; +import { getFirstEvent, getNextEvent, getPreviousEvent } from '../rundown-utils/rundownUtils.js'; import { isNumeric } from '../types/types.js'; /** @@ -9,7 +9,7 @@ import { isNumeric } from '../types/types.js'; */ export function getIncrement(input: string): string { // Check if the input string contains a number at the end - const match = input.match(/^(\D*)(\d+)(\.\d+)?$/); + const match = input?.match(/^(\D*)(\d+)(\.\d+)?$/); if (match) { // If a number is found, extract the non-numeric prefix, integer part, and decimal part @@ -61,15 +61,26 @@ export function getCueCandidate(rundown: OntimeRundown, insertAfterId?: string): } // get elements around - const previousEvent = rundown.at(afterIndex); + let previousEvent: OntimeRundownEntry | undefined | null | OntimeEvent = rundown.at(afterIndex); + if (!isOntimeEvent(previousEvent)) { + previousEvent = getPreviousEvent(rundown, insertAfterId) as null | OntimeEvent; + } + + let cue = '1'; const nextEvent = getNextEvent(rundown, insertAfterId); // try and increment the cue - let cue = getIncrement((previousEvent as OntimeEvent).cue); + if (isOntimeEvent(previousEvent)) { + cue = getIncrement(previousEvent.cue); + } // if increment is clashing with next, we add a decimal instead if (cue === nextEvent?.cue) { - cue = (previousEvent as OntimeEvent).cue + '.1'; + if (previousEvent === null) { + cue = '0.1'; + } else { + cue = previousEvent.cue + '.1'; + } } return cue;