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) {
} variant='ontime-subtle-white'>
Cancel
-
+
);
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;