fix: add event (#499)

* refactor: remove guards on event creation

* style: minimum safe block size

* fix: insert after finds previous valid cue
This commit is contained in:
Carlos Valente
2023-08-26 21:45:51 +02:00
committed by GitHub
parent af99882919
commit cd9dbcdc68
11 changed files with 119 additions and 27 deletions
@@ -6,11 +6,12 @@
background-color: $block-bg2; background-color: $block-bg2;
min-width: 34rem;
display: grid; display: grid;
grid-template-columns: 32px 1fr auto; grid-template-columns: 2rem 1fr auto;
align-items: center; align-items: center;
height: $secondary-block-height; height: $secondary-block-height;
gap: 8px; gap: 0.5rem;
&.hasCursor { &.hasCursor {
outline: 1px solid $block-cursor-color; outline: 1px solid $block-cursor-color;
@@ -59,7 +59,7 @@ export default function BlockBlock(props: BlockBlockProps) {
<IoReorderTwo /> <IoReorderTwo />
</span> </span>
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' /> <EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
<BlockActionMenu className={style.actionMenu} showAdd showDelay enableDelete actionHandler={actionHandler} /> <BlockActionMenu className={style.actionMenu} enableDelete actionHandler={actionHandler} />
</div> </div>
); );
} }
@@ -6,12 +6,13 @@
background-color: $block-bg2; background-color: $block-bg2;
min-width: 34rem;
display: grid; display: grid;
grid-template-columns: 32px 1fr auto; grid-template-columns: 2rem 1fr auto;
grid-template-areas: 'drag inpt btns'; grid-template-areas: 'drag inpt btns';
align-items: center; align-items: center;
height: $secondary-block-height; height: $secondary-block-height;
gap: 8px; gap: 0.5rem;
&.hasCursor { &.hasCursor {
outline: 1px solid $block-cursor-color; outline: 1px solid $block-cursor-color;
@@ -79,7 +79,7 @@ export default function DelayBlock(props: DelayBlockProps) {
<Button onClick={cancelDelayHandler} size='sm' leftIcon={<IoClose />} variant='ontime-subtle-white'> <Button onClick={cancelDelayHandler} size='sm' leftIcon={<IoClose />} variant='ontime-subtle-white'>
Cancel Cancel
</Button> </Button>
<BlockActionMenu showAdd enableDelete actionHandler={actionHandler} /> <BlockActionMenu enableDelete actionHandler={actionHandler} />
</HStack> </HStack>
</div> </div>
); );
@@ -144,7 +144,10 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
</Tooltip> </Tooltip>
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}> <Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
<span> <span>
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} /> <IoPeople
className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`}
data-isPublic={isPublic}
/>
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
@@ -163,7 +166,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
color={isOpen ? 'white' : '#f6f6f6'} color={isOpen ? 'white' : '#f6f6f6'}
isDisabled={disableEdit} isDisabled={disableEdit}
/> />
<BlockActionMenu showAdd showDelay showBlock showClone enableDelete={!selected} actionHandler={actionHandler} /> <BlockActionMenu showClone enableDelete={!selected} actionHandler={actionHandler} />
</div> </div>
</> </>
); );
@@ -11,9 +11,6 @@ import { tooltipDelayMid } from '../../../../ontimeConfig';
import { EventItemActions } from '../../RundownEntry'; import { EventItemActions } from '../../RundownEntry';
interface BlockActionMenuProps { interface BlockActionMenuProps {
showAdd?: boolean;
showDelay?: boolean;
showBlock?: boolean;
enableDelete?: boolean; enableDelete?: boolean;
showClone?: boolean; showClone?: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void; actionHandler: (action: EventItemActions, payload?: any) => void;
@@ -21,7 +18,7 @@ interface BlockActionMenuProps {
} }
export default function BlockActionMenu(props: 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 handleAddEvent = useCallback(() => actionHandler('event'), [actionHandler]);
const handleAddDelay = useCallback(() => actionHandler('delay'), [actionHandler]); const handleAddDelay = useCallback(() => actionHandler('delay'), [actionHandler]);
@@ -43,17 +40,17 @@ export default function BlockActionMenu(props: BlockActionMenuProps) {
/> />
</Tooltip> </Tooltip>
<MenuList> <MenuList>
<MenuItem icon={<IoAdd />} onClick={handleAddEvent} isDisabled={!showAdd}> <MenuItem icon={<IoAdd />} onClick={handleAddEvent}>
Add Event after Add Event after
</MenuItem> </MenuItem>
<MenuItem icon={<IoTimerOutline />} onClick={handleAddDelay} isDisabled={!showDelay}> <MenuItem icon={<IoTimerOutline />} onClick={handleAddDelay}>
Add Delay after Add Delay after
</MenuItem> </MenuItem>
<MenuItem icon={<IoRemoveCircleOutline />} onClick={handleAddBlock} isDisabled={!showBlock}> <MenuItem icon={<IoRemoveCircleOutline />} onClick={handleAddBlock}>
Add Block after Add Block after
</MenuItem> </MenuItem>
{showClone && ( {showClone && (
<MenuItem icon={<IoDuplicateOutline />} onClick={handleClone} isDisabled={!showBlock}> <MenuItem icon={<IoDuplicateOutline />} onClick={handleClone}>
Clone event Clone event
</MenuItem> </MenuItem>
)} )}
@@ -80,6 +80,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
size='xs' size='xs'
variant='ontime-subtle-white' variant='ontime-subtle-white'
className={style.quickBtn} className={style.quickBtn}
data-testId='quick-add-event'
> >
Event {showKbd && <span className={style.keyboard}>Alt + E</span>} Event {showKbd && <span className={style.keyboard}>Alt + E</span>}
</Button> </Button>
@@ -91,6 +92,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
variant='ontime-subtle-white' variant='ontime-subtle-white'
disabled={disableAddDelay} disabled={disableAddDelay}
className={style.quickBtn} className={style.quickBtn}
data-testId='quick-add-delay'
> >
Delay {showKbd && <span className={style.keyboard}>Alt + D</span>} Delay {showKbd && <span className={style.keyboard}>Alt + D</span>}
</Button> </Button>
@@ -102,6 +104,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
variant='ontime-subtle-white' variant='ontime-subtle-white'
disabled={disableAddBlock} disabled={disableAddBlock}
className={style.quickBtn} className={style.quickBtn}
data-testId='quick-add-block'
> >
Block {showKbd && <span className={style.keyboard}>Alt + B</span>} Block {showKbd && <span className={style.keyboard}>Alt + B</span>}
</Button> </Button>
@@ -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');
});
+5 -3
View File
@@ -1,14 +1,16 @@
import { OntimeRundownEntry } from '../definitions/core/Rundown.type.js'; import { OntimeRundownEntry } from '../definitions/core/Rundown.type.js';
import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from '../definitions/core/OntimeEvent.type.js'; import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from '../definitions/core/OntimeEvent.type.js';
export function isOntimeEvent(event: OntimeRundownEntry | Partial<OntimeRundownEntry>): event is OntimeEvent { type MaybeEvent = OntimeRundownEntry | null | undefined;
export function isOntimeEvent(event: MaybeEvent): event is OntimeEvent {
return event?.type === SupportedEvent.Event; return event?.type === SupportedEvent.Event;
} }
export function isOntimeDelay(event: OntimeRundownEntry | Partial<OntimeRundownEntry>): event is OntimeDelay { export function isOntimeDelay(event: MaybeEvent): event is OntimeDelay {
return event?.type === SupportedEvent.Delay; return event?.type === SupportedEvent.Delay;
} }
export function isOntimeBlock(event: OntimeRundownEntry | Partial<OntimeRundownEntry>): event is OntimeBlock { export function isOntimeBlock(event: MaybeEvent): event is OntimeBlock {
return event?.type === SupportedEvent.Block; return event?.type === SupportedEvent.Block;
} }
+20 -2
View File
@@ -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'; import { getCueCandidate, getIncrement, sanitiseCue } from './cueUtils.js';
@@ -32,7 +32,7 @@ describe('getIncrement()', () => {
}); });
}); });
describe('findCueName()', () => { describe('getCueCandidate()', () => {
describe('in the beginning of the rundown', () => { describe('in the beginning of the rundown', () => {
it('names cue as 1 if next event does not collide', () => { it('names cue as 1 if next event does not collide', () => {
const testRundown = [ const testRundown = [
@@ -89,6 +89,24 @@ describe('findCueName()', () => {
expect(cue).toBe('Presenter1.1'); 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', () => { describe('findCueName() with mixed events', () => {
+17 -6
View File
@@ -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'; import { isNumeric } from '../types/types.js';
/** /**
@@ -9,7 +9,7 @@ import { isNumeric } from '../types/types.js';
*/ */
export function getIncrement(input: string): string { export function getIncrement(input: string): string {
// Check if the input string contains a number at the end // 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 (match) {
// If a number is found, extract the non-numeric prefix, integer part, and decimal part // 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 // 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); const nextEvent = getNextEvent(rundown, insertAfterId);
// try and increment the cue // 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 increment is clashing with next, we add a decimal instead
if (cue === nextEvent?.cue) { if (cue === nextEvent?.cue) {
cue = (previousEvent as OntimeEvent).cue + '.1'; if (previousEvent === null) {
cue = '0.1';
} else {
cue = previousEvent.cue + '.1';
}
} }
return cue; return cue;