From 96280e5932af33e86a8bb00e9da0eedb30ef6fed Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Thu, 28 Dec 2023 13:41:43 +0100 Subject: [PATCH] Feat: reorder events with alt+ctrl + arrow up/down (#645) * fix: key guard * feat: reorder events * refactor: return index from the getEvent functions --- .../client/src/features/operator/Operator.tsx | 4 +- apps/client/src/features/rundown/Rundown.tsx | 27 ++++- packages/utils/src/cue-utils/cueUtils.ts | 6 +- .../src/rundown-utils/rundownUtils.test.ts | 106 +++++++++++++++--- .../utils/src/rundown-utils/rundownUtils.ts | 92 +++++++++------ 5 files changed, 176 insertions(+), 59 deletions(-) diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index c5ac43ace..ab1c38592 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -136,8 +136,8 @@ export default function Operator() { let isPast = Boolean(featureData.selectedEventId); const hidePast = isStringBoolean(searchParams.get('hidepast')); - const firstEvent = getFirstEvent(data); - const lastEvent = getLastEvent(data); + const { firstEvent } = getFirstEvent(data); + const { lastEvent } = getLastEvent(data); return (
diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index e4601d981..25b4f5fce 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -88,14 +88,16 @@ export default function Rundown(props: RundownProps) { (event: KeyboardEvent) => { // handle held key if (event.repeat) return; - // Check if the alt key is pressed - if (event.altKey && (!event.ctrlKey || !event.shiftKey)) { + // Check if the modifier combination + const modKeysAlt = event.altKey && !event.ctrlKey && !event.shiftKey; + const modKeysCtrlAlt = event.altKey && event.ctrlKey && !event.shiftKey; + if (modKeysAlt) { switch (event.code) { case 'ArrowDown': { if (entries.length < 1) { return; } - const nextEvent = cursor == null ? getFirst(entries) : getNext(entries, cursor); + const nextEvent = cursor == null ? getFirst(entries) : getNext(entries, cursor)?.nextEvent; if (nextEvent) { moveCursorTo(nextEvent.id, nextEvent.type === SupportedEvent.Event); } @@ -106,7 +108,7 @@ export default function Rundown(props: RundownProps) { return; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before - const previousEvent = cursor == null ? getFirst(entries) : getPrevious(entries, cursor); + const previousEvent = cursor == null ? getFirst(entries) : getPrevious(entries, cursor).previousEvent; if (previousEvent) { moveCursorTo(previousEvent.id, previousEvent.type === SupportedEvent.Event); } @@ -133,9 +135,24 @@ export default function Rundown(props: RundownProps) { break; } } + } else if (modKeysCtrlAlt) { + if (entries.length < 2 || cursor == null) { + return; + } + if (event.code == 'ArrowDown') { + const { nextEvent, nextIndex } = getNext(entries, cursor); + if (nextEvent && nextIndex !== null) { + reorderEvent(cursor, nextIndex - 1, nextIndex); + } + } else if (event.code == 'ArrowUp') { + const { previousEvent, previousIndex } = getPrevious(entries, cursor); + if (previousEvent && previousIndex !== null) { + reorderEvent(cursor, previousIndex + 1, previousIndex); + } + } } }, - [cursor, entries, insertAtCursor, moveCursorTo], + [cursor, entries, insertAtCursor, moveCursorTo, reorderEvent], ); // we copy the state from the store here diff --git a/packages/utils/src/cue-utils/cueUtils.ts b/packages/utils/src/cue-utils/cueUtils.ts index 0fa66409a..3f5c75a58 100644 --- a/packages/utils/src/cue-utils/cueUtils.ts +++ b/packages/utils/src/cue-utils/cueUtils.ts @@ -45,7 +45,7 @@ export function getIncrement(input: string): string { */ export function getCueCandidate(rundown: OntimeRundown, insertAfterId?: string): string { function addAtTop() { - const firstEventCue = getFirstEvent(rundown)?.cue; + const firstEventCue = getFirstEvent(rundown).firstEvent?.cue; if (isNumeric(firstEventCue)) { return (Number(firstEventCue) / 10).toString(); @@ -68,11 +68,11 @@ export function getCueCandidate(rundown: OntimeRundown, insertAfterId?: string): // get elements around let previousEvent: OntimeRundownEntry | undefined | null | OntimeEvent = rundown.at(afterIndex); if (!isOntimeEvent(previousEvent)) { - previousEvent = getPreviousEvent(rundown, insertAfterId) as null | OntimeEvent; + previousEvent = getPreviousEvent(rundown, insertAfterId).previousEvent as null | OntimeEvent; } let cue = '1'; - const nextEvent = getNextEvent(rundown, insertAfterId); + const { nextEvent } = getNextEvent(rundown, insertAfterId); // try and increment the cue if (isOntimeEvent(previousEvent)) { diff --git a/packages/utils/src/rundown-utils/rundownUtils.test.ts b/packages/utils/src/rundown-utils/rundownUtils.test.ts index 892732965..bb5cf0175 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.test.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.test.ts @@ -1,6 +1,43 @@ import { OntimeRundown, SupportedEvent } from 'ontime-types'; -import { getNextEvent, getPreviousEvent } from './rundownUtils'; +import { getNext, getNextEvent, getPrevious, getPreviousEvent } from './rundownUtils'; + +describe('getNext()', () => { + it('returns the next event of type event', () => { + const testRundown = [ + { id: '1', type: SupportedEvent.Event }, + { id: '2', type: SupportedEvent.Event }, + { id: '3', type: SupportedEvent.Event }, + ]; + + const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '1'); + expect(nextEvent?.id).toBe('2'); + expect(nextIndex).toBe(1); + }); + it('alows other event types', () => { + const testRundown = [ + { id: '1', type: SupportedEvent.Event }, + { id: '2', type: SupportedEvent.Delay }, + { id: '3', type: SupportedEvent.Block }, + { id: '4', type: SupportedEvent.Event }, + ]; + + const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '1'); + expect(nextEvent?.id).toBe('2'); + expect(nextIndex).toBe(1); + }); + it('returns null if none found', () => { + const testRundown = [ + { id: '1', type: SupportedEvent.Event }, + { id: '2', type: SupportedEvent.Delay }, + { id: '3', type: SupportedEvent.Block }, + ]; + + const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '3'); + expect(nextEvent).toBe(null); + expect(nextIndex).toBe(null); + }); +}); describe('getNextEvent()', () => { it('returns the next event of type event', () => { @@ -10,8 +47,9 @@ describe('getNextEvent()', () => { { id: '3', type: SupportedEvent.Event }, ]; - const next = getNextEvent(testRundown as OntimeRundown, '1'); - expect(next?.id).toBe('2'); + const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1'); + expect(nextEvent?.id).toBe('2'); + expect(nextIndex).toBe(1); }); it('ignores other event types', () => { const testRundown = [ @@ -21,8 +59,9 @@ describe('getNextEvent()', () => { { id: '4', type: SupportedEvent.Event }, ]; - const next = getNextEvent(testRundown as OntimeRundown, '1'); - expect(next?.id).toBe('4'); + const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1'); + expect(nextEvent?.id).toBe('4'); + expect(nextIndex).toBe(3); }); it('returns null if none found', () => { const testRundown = [ @@ -31,8 +70,46 @@ describe('getNextEvent()', () => { { id: '3', type: SupportedEvent.Block }, ]; - const next = getNextEvent(testRundown as OntimeRundown, '1'); - expect(next).toBe(null); + const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1'); + expect(nextEvent).toBe(null); + expect(nextIndex).toBe(null); + }); +}); + +describe('getPrevious()', () => { + it('returns the previous event of type event', () => { + const testRundown = [ + { id: '1', type: SupportedEvent.Event }, + { id: '2', type: SupportedEvent.Event }, + { id: '3', type: SupportedEvent.Event }, + ]; + + const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '3'); + expect(previousEvent?.id).toBe('2'); + expect(previousIndex).toBe(1); + }); + it('allow other event types', () => { + const testRundown = [ + { id: '1', type: SupportedEvent.Event }, + { id: '2', type: SupportedEvent.Delay }, + { id: '3', type: SupportedEvent.Block }, + { id: '4', type: SupportedEvent.Event }, + ]; + + const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '3'); + expect(previousEvent?.id).toBe('2'); + expect(previousIndex).toBe(1); + }); + it('returns null if none found', () => { + const testRundown = [ + { id: '2', type: SupportedEvent.Delay }, + { id: '3', type: SupportedEvent.Block }, + { id: '4', type: SupportedEvent.Event }, + ]; + + const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '2'); + expect(previousEvent).toBe(null); + expect(previousIndex).toBe(null); }); }); @@ -44,8 +121,9 @@ describe('getPreviousEvent()', () => { { id: '3', type: SupportedEvent.Event }, ]; - const previous = getPreviousEvent(testRundown as OntimeRundown, '3'); - expect(previous?.id).toBe('2'); + const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '3'); + expect(previousEvent?.id).toBe('2'); + expect(previousIndex).toBe(1); }); it('ignores other event types', () => { const testRundown = [ @@ -55,8 +133,9 @@ describe('getPreviousEvent()', () => { { id: '4', type: SupportedEvent.Event }, ]; - const previous = getPreviousEvent(testRundown as OntimeRundown, '4'); - expect(previous?.id).toBe('1'); + const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '4'); + expect(previousEvent?.id).toBe('1'); + expect(previousIndex).toBe(0); }); it('returns null if none found', () => { const testRundown = [ @@ -65,7 +144,8 @@ describe('getPreviousEvent()', () => { { id: '4', type: SupportedEvent.Event }, ]; - const previous = getNextEvent(testRundown as OntimeRundown, '1'); - expect(previous).toBe(null); + const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '2'); + expect(previousEvent).toBe(null); + expect(previousIndex).toBe(null); }); }); diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts index dbf75ad25..a887dd0b1 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.ts @@ -12,45 +12,55 @@ export function getFirst(rundown: OntimeRundownEntry[]) { /** * Gets first scheduled event in rundown, if it exists * @param {OntimeRundownEntry[]} rundown - * @return {OntimeEvent | null} + * @return {{ firstEvent: OntimeEvent | null; firstIndex: number | null } } */ -export function getFirstEvent(rundown: OntimeRundownEntry[]) { +export function getFirstEvent(rundown: OntimeRundownEntry[]): { + firstEvent: OntimeEvent | null; + firstIndex: number | null; +} { for (let i = 0; i < rundown.length; i++) { - const event = rundown[i]; - if (isOntimeEvent(event)) { - return event; + const firstEvent = rundown[i]; + if (isOntimeEvent(firstEvent)) { + return { firstEvent, firstIndex: i }; } } - return null; + return { firstEvent: null, firstIndex: null }; } -export function getLastEvent(rundown: OntimeRundown): OntimeEvent | null { +export function getLastEvent(rundown: OntimeRundown): { + lastEvent: OntimeEvent | null; + lastIndex: number | null; +} { if (rundown.length < 1) { - return null; + return { lastEvent: null, lastIndex: null }; } for (let i = rundown.length - 1; i > 0; i--) { - const event = rundown.at(i); - if (isOntimeEvent(event)) { - return event; + const lastEvent = rundown.at(i); + if (isOntimeEvent(lastEvent)) { + return { lastEvent, lastIndex: i }; } } - - return null; + return { lastEvent: null, lastIndex: null }; } /** * Gets next event in rundown, if it exists * @param {OntimeRundownEntry[]} rundown * @param {string} currentId - * @return {OntimeRundownEntry | null} + * @return {{ nextEvent: OntimeRundownEntry | null; nextIndex: number | null } } */ -export function getNext(rundown: OntimeRundownEntry[], currentId: string): OntimeRundownEntry | null { +export function getNext( + rundown: OntimeRundownEntry[], + currentId: string, +): { nextEvent: OntimeRundownEntry | null; nextIndex: number | null } { const index = rundown.findIndex((event) => event.id === currentId); if (index !== -1 && index + 1 < rundown.length) { - return rundown[index + 1]; + const nextIndex = index + 1; + const nextEvent = rundown[nextIndex]; + return { nextEvent, nextIndex }; } else { - return null; + return { nextEvent: null, nextIndex: null }; } } @@ -58,35 +68,43 @@ export function getNext(rundown: OntimeRundownEntry[], currentId: string): Ontim * Gets next scheduled event in rundown, if it exists * @param {OntimeRundownEntry[]} rundown * @param {string} currentId - * @return {OntimeEvent | null} + * @return {{ nextEvent: OntimeEvent | null; nextIndex: number | null } } */ -export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string): OntimeEvent | null { +export function getNextEvent( + rundown: OntimeRundownEntry[], + currentId: string, +): { nextEvent: OntimeEvent | null; nextIndex: number | null } { const index = rundown.findIndex((event) => event.id === currentId); if (index < 0) { - return null; + return { nextEvent: null, nextIndex: null }; } for (let i = index + 1; i < rundown.length; i++) { - const event = rundown[i]; - if (isOntimeEvent(event)) { - return event; + const nextEvent = rundown[i]; + if (isOntimeEvent(nextEvent)) { + return { nextEvent, nextIndex: i }; } } - return null; + return { nextEvent: null, nextIndex: null }; } /** * Gets previous event in rundown, if it exists * @param {OntimeRundownEntry[]} rundown * @param {string} currentId - * @return {OntimeRundownEntry | null} + * @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } } */ -export function getPrevious(rundown: OntimeRundownEntry[], currentId: string) { +export function getPrevious( + rundown: OntimeRundownEntry[], + currentId: string, +): { previousEvent: OntimeRundownEntry | null; previousIndex: number | null } { const index = rundown.findIndex((event) => event.id === currentId); if (index !== -1 && index - 1 >= 0) { - return rundown[index - 1]; + const previousIndex = index - 1; + const previousEvent = rundown[previousIndex]; + return { previousEvent, previousIndex }; } else { - return null; + return { previousEvent: null, previousIndex: null }; } } @@ -94,21 +112,23 @@ export function getPrevious(rundown: OntimeRundownEntry[], currentId: string) { * Gets previous scheduled event in rundown, if it exists * @param {OntimeRundownEntry[]} rundown * @param {string} currentId - * @return {OntimeEvent | null} + * @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } } */ -export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: string): OntimeEvent | null { +export function getPreviousEvent( + rundown: OntimeRundownEntry[], + currentId: string, +): { previousEvent: OntimeEvent | null; previousIndex: number | null } { const index = rundown.findIndex((event) => event.id === currentId); if (index < 0) { - return null; + return { previousEvent: null, previousIndex: null }; } - for (let i = index - 1; i >= 0; i--) { - const event = rundown[i]; - if (isOntimeEvent(event)) { - return event; + const previousEvent = rundown[i]; + if (isOntimeEvent(previousEvent)) { + return { previousEvent, previousIndex: i }; } } - return null; + return { previousEvent: null, previousIndex: null }; } /**