mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 22:49:18 +00:00
shortcuts using useHotkeys from mantine/hooks (#907)
--------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
0c3c08ac98
commit
9e04a1dcfa
@@ -1,27 +1,25 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { RefObject, useEffect } from 'react';
|
||||||
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
||||||
// @ts-expect-error no types from library
|
// @ts-expect-error no types from library
|
||||||
import autosize from 'autosize/dist/autosize';
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
|
||||||
export const AutoTextArea = (props: TextareaProps) => {
|
export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknown> }) => {
|
||||||
const { value } = props;
|
const { value, inputref } = props;
|
||||||
|
|
||||||
const ref = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = ref.current;
|
const node = inputref.current;
|
||||||
autosize(ref.current);
|
autosize(inputref.current);
|
||||||
return () => {
|
return () => {
|
||||||
autosize.destroy(node);
|
autosize.destroy(node);
|
||||||
};
|
};
|
||||||
}, [value]);
|
}, [inputref, value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
w='100%'
|
w='100%'
|
||||||
|
ref={inputref}
|
||||||
resize='none'
|
resize='none'
|
||||||
ref={ref}
|
|
||||||
transition='height none'
|
transition='height none'
|
||||||
variant='ontime-transparent'
|
variant='ontime-transparent'
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
|
||||||
import { Input, Textarea } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { EventEditorSubmitActions } from '../../../../features/rundown/event-editor/EventEditor';
|
|
||||||
import { Size } from '../../../models/Util.type';
|
|
||||||
|
|
||||||
import useReactiveTextInput from './useReactiveTextInput';
|
|
||||||
|
|
||||||
interface BaseProps {
|
|
||||||
isTextArea?: boolean;
|
|
||||||
isFullHeight?: boolean;
|
|
||||||
size?: Size;
|
|
||||||
field: EventEditorSubmitActions;
|
|
||||||
initialText?: string;
|
|
||||||
submitHandler: (field: EventEditorSubmitActions, newValue: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextInputProps extends BaseProps {
|
|
||||||
isTextArea?: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResizeOptions = 'horizontal' | 'vertical' | 'none';
|
|
||||||
|
|
||||||
interface TextAreaProps extends BaseProps {
|
|
||||||
isTextArea: true;
|
|
||||||
resize?: ResizeOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
type InputProps = TextInputProps | TextAreaProps;
|
|
||||||
|
|
||||||
export default function TextInput(props: InputProps) {
|
|
||||||
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler } = props;
|
|
||||||
const inputRef = useRef(null);
|
|
||||||
|
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
|
||||||
|
|
||||||
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
|
||||||
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
|
||||||
|
|
||||||
let resize: ResizeOptions = 'none';
|
|
||||||
if (isTextArea) {
|
|
||||||
resize = (props as TextAreaProps)?.resize ?? 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
return isTextArea ? (
|
|
||||||
<Textarea
|
|
||||||
ref={inputRef}
|
|
||||||
size={size}
|
|
||||||
resize={resize}
|
|
||||||
variant='ontime-filled'
|
|
||||||
{...textAreaProps}
|
|
||||||
style={{ height: isFullHeight ? '100%' : undefined }}
|
|
||||||
data-testid='input-textarea'
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input ref={inputRef} size={size} variant='ontime-filled' {...textInputProps} data-testid='input-textfield' />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks';
|
||||||
|
|
||||||
interface UseReactiveTextInputReturn {
|
interface UseReactiveTextInputReturn {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (event: ChangeEvent) => void;
|
onChange: (event: ChangeEvent) => void;
|
||||||
onBlur: (event: ChangeEvent) => void;
|
onBlur: (event: ChangeEvent) => void;
|
||||||
onKeyDown: (event: KeyboardEvent) => void;
|
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useReactiveTextInput(
|
export default function useReactiveTextInput(
|
||||||
initialText: string,
|
initialText: string,
|
||||||
submitCallback: (newValue: string) => void,
|
submitCallback: (newValue: string) => void,
|
||||||
|
ref: RefObject<HTMLInputElement>,
|
||||||
options?: {
|
options?: {
|
||||||
submitOnEnter?: boolean;
|
submitOnEnter?: boolean;
|
||||||
|
submitOnCtrlEnter?: boolean;
|
||||||
},
|
},
|
||||||
): UseReactiveTextInputReturn {
|
): UseReactiveTextInputReturn {
|
||||||
const [text, setText] = useState<string>(initialText);
|
const [text, setText] = useState<string>(initialText);
|
||||||
@@ -44,43 +47,45 @@ export default function useReactiveTextInput(
|
|||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(valueToSubmit: string) => {
|
(valueToSubmit: string) => {
|
||||||
// No need to update if it hasn't changed
|
// No need to update if it hasn't changed
|
||||||
if (valueToSubmit === initialText) {
|
if (valueToSubmit !== initialText) {
|
||||||
return;
|
const cleanVal = valueToSubmit.trim();
|
||||||
}
|
submitCallback(cleanVal);
|
||||||
const cleanVal = valueToSubmit.trim();
|
if (cleanVal !== valueToSubmit) {
|
||||||
submitCallback(cleanVal);
|
setText(cleanVal);
|
||||||
|
}
|
||||||
if (cleanVal !== valueToSubmit) {
|
|
||||||
setText(cleanVal);
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
||||||
},
|
},
|
||||||
[initialText, submitCallback],
|
[initialText, ref, submitCallback],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Handles common keys for submit and cancel
|
* @description Handles escape events
|
||||||
* @param {string} key
|
* @param {string} valueToSubmit
|
||||||
*/
|
*/
|
||||||
const keyHandler = useCallback(
|
const handleEscape = useCallback(() => {
|
||||||
(key: string) => {
|
// No need to update if it hasn't changed
|
||||||
switch (key) {
|
setText(initialText);
|
||||||
case 'Escape':
|
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
||||||
setText(initialText);
|
}, [initialText, ref]);
|
||||||
break;
|
|
||||||
case 'Enter':
|
const keyHandler = useMemo(() => {
|
||||||
if (options?.submitOnEnter) {
|
const hotKeys: HotkeyItem[] = [['Escape', handleEscape, { preventDefault: true }]];
|
||||||
handleSubmit(text);
|
|
||||||
}
|
if (options?.submitOnEnter) {
|
||||||
break;
|
hotKeys.push(['Enter', () => handleSubmit(text)]);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[initialText, options?.submitOnEnter, handleSubmit, text],
|
if (options?.submitOnCtrlEnter) {
|
||||||
);
|
hotKeys.push(['mod + Enter', () => handleSubmit(text)]);
|
||||||
|
}
|
||||||
|
return getHotkeyHandler(hotKeys);
|
||||||
|
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, text]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: text,
|
value: text,
|
||||||
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
||||||
onBlur: (event: ChangeEvent) => handleSubmit((event.target as HTMLInputElement).value),
|
onBlur: (event: ChangeEvent) => handleSubmit((event.target as HTMLInputElement).value),
|
||||||
onKeyDown: (event: KeyboardEvent) => keyHandler(event.key),
|
onKeyDown: keyHandler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { useEditorSettings } from '../stores/editorSettings';
|
|||||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Set of utilities for events
|
* @description Set of utilities for events //TODO: should this be called useEntryAction and so on
|
||||||
*/
|
*/
|
||||||
export const useEventAction = () => {
|
export const useEventAction = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ export const useAppMode = create<AppModeStore>()((set) => ({
|
|||||||
return { mode };
|
return { mode };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setCursor: (cursor: string | null) => set(() => ({ cursor })),
|
setCursor: (cursor: string | null) => set({ cursor }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type EntryCopyStore = {
|
||||||
|
entryCopyId: string | null;
|
||||||
|
setEntryCopyId: (eventId: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEntryCopy = create<EntryCopyStore>()((set) => ({
|
||||||
|
entryCopyId: null,
|
||||||
|
setEntryCopyId: (entryCopyId: string | null) => set({ entryCopyId }),
|
||||||
|
}));
|
||||||
@@ -4,3 +4,5 @@ export function isMacOS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const deviceAlt = isMacOS() ? '⌥' : 'Alt';
|
export const deviceAlt = isMacOS() ? '⌥' : 'Alt';
|
||||||
|
|
||||||
|
export const deviceMod = isMacOS() ? '⌘' : 'Ctrl';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ChangeEvent, memo, useCallback, useEffect, useState } from 'react';
|
import { ChangeEvent, memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { getHotkeyHandler } from '@mantine/hooks';
|
||||||
|
|
||||||
import { AutoTextArea } from '../../../common/components/input/auto-text-area/AutoTextArea';
|
import { AutoTextArea } from '../../../common/components/input/auto-text-area/AutoTextArea';
|
||||||
|
|
||||||
@@ -12,12 +13,24 @@ const EditableCell = (props: EditableCellProps) => {
|
|||||||
|
|
||||||
// We need to keep and update the state of the cell normally
|
// We need to keep and update the state of the cell normally
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
|
const ref = useRef<HTMLAreaElement>();
|
||||||
const onChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => setValue(event.target.value), []);
|
const onChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => setValue(event.target.value), []);
|
||||||
|
|
||||||
// We'll only update the external data when the input is blurred
|
// We'll only update the external data when the input is blurred
|
||||||
const onBlur = useCallback(() => handleUpdate(value), [handleUpdate, value]);
|
const onBlur = useCallback(() => handleUpdate(value), [handleUpdate, value]);
|
||||||
|
|
||||||
|
//TODO: maybe we can unify this with `useReactiveTextInput`
|
||||||
|
const onKeyDown = getHotkeyHandler([
|
||||||
|
['mod + Enter', () => ref.current?.blur()],
|
||||||
|
[
|
||||||
|
'Escape',
|
||||||
|
() => {
|
||||||
|
setValue(initialValue);
|
||||||
|
setTimeout(() => ref.current?.blur());
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
// If the initialValue is changed external, sync it up with our state
|
// If the initialValue is changed external, sync it up with our state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(initialValue);
|
setValue(initialValue);
|
||||||
@@ -27,9 +40,11 @@ const EditableCell = (props: EditableCellProps) => {
|
|||||||
<AutoTextArea
|
<AutoTextArea
|
||||||
size='sm'
|
size='sm'
|
||||||
value={value}
|
value={value}
|
||||||
|
inputref={ref}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
rows={1}
|
rows={1}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
transition='none'
|
transition='none'
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { isOntimeEvent, MaybeNumber, Playback, RundownCached, SupportedEvent } from 'ontime-types';
|
import { isOntimeEvent, MaybeNumber, Playback, RundownCached, SupportedEvent } from 'ontime-types';
|
||||||
import { getFirstNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
|
import { getFirstNormal, getLastNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||||
|
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||||
|
|
||||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||||
@@ -26,7 +28,9 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
const [statefulEntries, setStatefulEntries] = useState(order);
|
const [statefulEntries, setStatefulEntries] = useState(order);
|
||||||
|
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
const { addEvent, reorderEvent } = useEventAction();
|
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
||||||
|
|
||||||
|
const { entryCopyId, setEntryCopyId } = useEntryCopy();
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
const { cursor, mode: appMode, setCursor } = useAppMode();
|
const { cursor, mode: appMode, setCursor } = useAppMode();
|
||||||
@@ -37,9 +41,21 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
// DND KIT
|
// DND KIT
|
||||||
const sensors = useSensors(useSensor(PointerSensor));
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
|
|
||||||
|
const deleteAtCursor = useCallback(
|
||||||
|
(cursor: string | null) => {
|
||||||
|
if (!cursor) return;
|
||||||
|
const previous = getPreviousNormal(rundown, order, cursor).entry?.id ?? null;
|
||||||
|
deleteEvent(cursor);
|
||||||
|
setCursor(previous);
|
||||||
|
},
|
||||||
|
[deleteEvent, order, rundown, setCursor],
|
||||||
|
);
|
||||||
|
|
||||||
const insertAtCursor = useCallback(
|
const insertAtCursor = useCallback(
|
||||||
(type: SupportedEvent | 'clone', cursor: string | null) => {
|
(type: SupportedEvent | 'clone', cursor: string | null, above = false) => {
|
||||||
if (cursor === null) {
|
const adjustedCursor = above ? getPreviousNormal(rundown, order, cursor ?? '').entry?.id ?? null : cursor;
|
||||||
|
|
||||||
|
if (adjustedCursor === null) {
|
||||||
// we cant clone without selection
|
// we cant clone without selection
|
||||||
if (type === 'clone') {
|
if (type === 'clone') {
|
||||||
return;
|
return;
|
||||||
@@ -50,7 +66,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'clone') {
|
if (type === 'clone') {
|
||||||
const cursorEvent = rundown[cursor];
|
const cursorEvent = rundown[adjustedCursor];
|
||||||
if (cursorEvent?.type === SupportedEvent.Event) {
|
if (cursorEvent?.type === SupportedEvent.Event) {
|
||||||
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
||||||
addEvent(newEvent);
|
addEvent(newEvent);
|
||||||
@@ -60,111 +76,90 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
};
|
};
|
||||||
const options = {
|
const options = {
|
||||||
after: cursor,
|
after: adjustedCursor,
|
||||||
lastEventId: cursor,
|
lastEventId: adjustedCursor,
|
||||||
};
|
};
|
||||||
addEvent(newEvent, options);
|
addEvent(newEvent, options);
|
||||||
} else {
|
} else {
|
||||||
addEvent({ type }, { after: cursor });
|
addEvent({ type }, { after: adjustedCursor });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addEvent, rundown],
|
[rundown, order, addEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
const selectEntry = useCallback(
|
||||||
const handleKeyDown = useCallback(
|
(cursor: string | null, direction: 'up' | 'down') => {
|
||||||
(event: KeyboardEvent) => {
|
if (order.length < 1) {
|
||||||
// handle held key
|
return;
|
||||||
if (event.repeat) return;
|
}
|
||||||
|
let newCursor: string | undefined;
|
||||||
|
if (cursor === null) {
|
||||||
|
// there is no cursor, we select the first or last depending on direction if it exists
|
||||||
|
newCursor = direction === 'up' ? getLastNormal(rundown, order)?.id : getFirstNormal(rundown, order)?.id;
|
||||||
|
} else {
|
||||||
|
// otherwise we select the next or previous
|
||||||
|
newCursor =
|
||||||
|
direction === 'up'
|
||||||
|
? getPreviousNormal(rundown, order, cursor).entry?.id
|
||||||
|
: getNextNormal(rundown, order, cursor).entry?.id;
|
||||||
|
}
|
||||||
|
|
||||||
const modKeysAlt = event.altKey && !event.ctrlKey && !event.shiftKey;
|
if (newCursor) {
|
||||||
const modKeysCtrlAlt = event.altKey && event.ctrlKey && !event.shiftKey;
|
setCursor(newCursor);
|
||||||
|
|
||||||
if (modKeysAlt) {
|
|
||||||
switch (event.code) {
|
|
||||||
case 'ArrowDown': {
|
|
||||||
if (order.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextEvent =
|
|
||||||
cursor === null ? getFirstNormal(rundown, order) : getNextNormal(rundown, order, cursor)?.nextEvent;
|
|
||||||
if (nextEvent) {
|
|
||||||
setCursor(nextEvent.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ArrowUp': {
|
|
||||||
if (order.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
|
||||||
const previousEvent =
|
|
||||||
cursor === null
|
|
||||||
? getFirstNormal(rundown, order)
|
|
||||||
: getPreviousNormal(rundown, order, cursor).previousEvent;
|
|
||||||
if (previousEvent) {
|
|
||||||
setCursor(previousEvent.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KeyE': {
|
|
||||||
event.preventDefault();
|
|
||||||
insertAtCursor(SupportedEvent.Event, cursor);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KeyD': {
|
|
||||||
event.preventDefault();
|
|
||||||
insertAtCursor(SupportedEvent.Delay, cursor);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KeyB': {
|
|
||||||
event.preventDefault();
|
|
||||||
insertAtCursor(SupportedEvent.Block, cursor);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KeyC': {
|
|
||||||
event.preventDefault();
|
|
||||||
insertAtCursor('clone', cursor);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (modKeysCtrlAlt) {
|
|
||||||
if (order.length < 2 || cursor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Alt + Ctrl + Arrow Down
|
|
||||||
if (event.code == 'ArrowDown') {
|
|
||||||
const { nextEvent, nextIndex } = getNextNormal(rundown, order, cursor);
|
|
||||||
if (nextEvent && nextIndex !== null) {
|
|
||||||
reorderEvent(cursor, nextIndex - 1, nextIndex);
|
|
||||||
}
|
|
||||||
// Alt + Ctrl + Arrow Up
|
|
||||||
} else if (event.code == 'ArrowUp') {
|
|
||||||
const { previousEvent, previousIndex } = getPreviousNormal(rundown, order, cursor);
|
|
||||||
if (previousEvent && previousIndex !== null) {
|
|
||||||
reorderEvent(cursor, previousIndex + 1, previousIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[order, cursor, rundown, setCursor, insertAtCursor, reorderEvent],
|
[order, rundown, setCursor],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const moveEntry = useCallback(
|
||||||
|
(cursor: string | null, direction: 'up' | 'down') => {
|
||||||
|
if (order.length < 2 || cursor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { index } =
|
||||||
|
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
||||||
|
|
||||||
|
if (index !== null) {
|
||||||
|
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
||||||
|
reorderEvent(cursor, offsetIndex, index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[order, reorderEvent, rundown],
|
||||||
|
);
|
||||||
|
|
||||||
|
// shortcuts
|
||||||
|
useHotkeys([
|
||||||
|
['alt + ArrowDown', () => selectEntry(cursor, 'down'), { preventDefault: true }],
|
||||||
|
['alt + ArrowUp', () => selectEntry(cursor, 'up'), { preventDefault: true }],
|
||||||
|
['alt + mod + ArrowDown', () => moveEntry(cursor, 'down'), { preventDefault: true }],
|
||||||
|
['alt + mod + ArrowUp', () => moveEntry(cursor, 'up'), { preventDefault: true }],
|
||||||
|
|
||||||
|
['Escape', () => setCursor(null), { preventDefault: true }],
|
||||||
|
|
||||||
|
['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
|
||||||
|
|
||||||
|
['alt + E', () => insertAtCursor(SupportedEvent.Event, cursor), { preventDefault: true }],
|
||||||
|
['alt + shift + E', () => insertAtCursor(SupportedEvent.Event, cursor, true), { preventDefault: true }],
|
||||||
|
|
||||||
|
['alt + B', () => insertAtCursor(SupportedEvent.Block, cursor), { preventDefault: true }],
|
||||||
|
['alt + shift + B', () => insertAtCursor(SupportedEvent.Block, cursor, true), { preventDefault: true }],
|
||||||
|
|
||||||
|
['alt + D', () => insertAtCursor(SupportedEvent.Delay, cursor), { preventDefault: true }],
|
||||||
|
['alt + shift + D', () => insertAtCursor(SupportedEvent.Delay, cursor, true), { preventDefault: true }],
|
||||||
|
|
||||||
|
['mod + C', () => setEntryCopyId(cursor), { preventDefault: true }],
|
||||||
|
['mod + V', () => insertAtCursor('clone', entryCopyId), { preventDefault: true }],
|
||||||
|
['mod + shift + V', () => insertAtCursor('clone', entryCopyId, true), { preventDefault: true }],
|
||||||
|
|
||||||
|
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
|
||||||
|
]);
|
||||||
|
|
||||||
// we copy the state from the store here
|
// we copy the state from the store here
|
||||||
// to workaround async updates on the drag mutations
|
// to workaround async updates on the drag mutations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStatefulEntries(order);
|
setStatefulEntries(order);
|
||||||
}, [order]);
|
}, [order]);
|
||||||
|
|
||||||
// listen to keys
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [handleKeyDown]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// in run mode, we follow selection
|
// in run mode, we follow selection
|
||||||
if (appMode !== AppMode.Run || !featureData?.selectedEventId) {
|
if (appMode !== AppMode.Run || !featureData?.selectedEventId) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Input } from '@chakra-ui/react';
|
import { Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
@@ -17,7 +17,7 @@ interface TitleEditorProps {
|
|||||||
export default function EditableBlockTitle(props: TitleEditorProps) {
|
export default function EditableBlockTitle(props: TitleEditorProps) {
|
||||||
const { title, eventId, placeholder, className } = props;
|
const { title, eventId, placeholder, className } = props;
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback(
|
const submitCallback = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
if (text === title) {
|
if (text === title) {
|
||||||
@@ -30,7 +30,7 @@ export default function EditableBlockTitle(props: TitleEditorProps) {
|
|||||||
[title, updateEvent, eventId],
|
[title, updateEvent, eventId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(title, submitCallback, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(title, submitCallback, ref, {
|
||||||
submitOnEnter: true,
|
submitOnEnter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ export default function EditableBlockTitle(props: TitleEditorProps) {
|
|||||||
<Input
|
<Input
|
||||||
data-testid='block__title'
|
data-testid='block__title'
|
||||||
variant='ontime-ghosted'
|
variant='ontime-ghosted'
|
||||||
|
ref={ref}
|
||||||
value={value}
|
value={value}
|
||||||
className={classes}
|
className={classes}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useEventSelection } from '../useEventSelection';
|
|||||||
import EventEditorTimes from './composite/EventEditorTimes';
|
import EventEditorTimes from './composite/EventEditorTimes';
|
||||||
import EventEditorTitles from './composite/EventEditorTitles';
|
import EventEditorTitles from './composite/EventEditorTitles';
|
||||||
import EventTextArea from './composite/EventTextArea';
|
import EventTextArea from './composite/EventTextArea';
|
||||||
|
import EventEditorEmpty from './EventEditorEmpty';
|
||||||
|
|
||||||
import style from './EventEditor.module.scss';
|
import style from './EventEditor.module.scss';
|
||||||
|
|
||||||
@@ -67,11 +68,7 @@ export default function EventEditor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return (
|
return <EventEditorEmpty />;
|
||||||
<div className={style.eventEditor} data-testid='editor-container'>
|
|
||||||
Select an event to edit
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.eventEditor {
|
||||||
|
color: $label-gray;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcutSection {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 1rem 0;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
td:nth-child(odd) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { Kbd } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
|
||||||
|
|
||||||
|
import style from './EventEditorEmpty.module.scss';
|
||||||
|
|
||||||
|
export default memo(EventEditorEmpty);
|
||||||
|
|
||||||
|
function EventEditorEmpty() {
|
||||||
|
return (
|
||||||
|
<div className={style.eventEditor} data-testid='editor-container'>
|
||||||
|
<div className={style.prompt}>Select an event to edit</div>
|
||||||
|
<div className={style.shortcutSection}>
|
||||||
|
<div className={style.prompt}>Shortcut navigation</div>
|
||||||
|
|
||||||
|
<table className={style.shortcuts}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> / <Kbd>↓</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Select entry</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>{deviceMod}</Kbd> + <Kbd>↑</Kbd> / <Kbd>↑</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Reorder selected entry</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>E</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Add event below</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> + <Kbd>E</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Add event above</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>B</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Add block below</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> + <Kbd>B</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Add block above</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>D</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Add delay below</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> + <Kbd>D</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Add delay above</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>Esc</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Deselect entry</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd> + <Kbd>⌫</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Delete selected entry</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd> + <Kbd>C</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Copy selected entry</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd> + <Kbd>↑</Kbd> + <Kbd>V</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Paste above</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd> + <Kbd>V</Kbd>
|
||||||
|
</td>
|
||||||
|
<td>Paste below</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CSSProperties, useCallback } from 'react';
|
import { CSSProperties, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
@@ -18,10 +18,12 @@ interface CountedTextAreaProps {
|
|||||||
|
|
||||||
export default function EventTextArea(props: CountedTextAreaProps) {
|
export default function EventTextArea(props: CountedTextAreaProps) {
|
||||||
const { className, field, label, initialValue, style: givenStyles, submitHandler } = props;
|
const { className, field, label, initialValue, style: givenStyles, submitHandler } = props;
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback);
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
|
submitOnCtrlEnter: true,
|
||||||
|
});
|
||||||
const classes = cx([style.inputLabel, className]);
|
const classes = cx([style.inputLabel, className]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -31,6 +33,7 @@ export default function EventTextArea(props: CountedTextAreaProps) {
|
|||||||
</label>
|
</label>
|
||||||
<AutoTextArea
|
<AutoTextArea
|
||||||
id={field}
|
id={field}
|
||||||
|
inputref={ref}
|
||||||
rows={1}
|
rows={1}
|
||||||
size='sm'
|
size='sm'
|
||||||
resize='none'
|
resize='none'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Input, InputProps } from '@chakra-ui/react';
|
import { Input, InputProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
@@ -15,10 +15,10 @@ interface CountedTextInputProps extends InputProps {
|
|||||||
|
|
||||||
export default function EventTextInput(props: CountedTextInputProps) {
|
export default function EventTextInput(props: CountedTextInputProps) {
|
||||||
const { field, label, initialValue, submitHandler, maxLength } = props;
|
const { field, label, initialValue, submitHandler, maxLength } = props;
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
submitOnEnter: true,
|
submitOnEnter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export default function EventTextInput(props: CountedTextInputProps) {
|
|||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id={field}
|
id={field}
|
||||||
|
ref={ref}
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
data-testid='input-textfield'
|
data-testid='input-textfield'
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ const theme = extendTheme({
|
|||||||
'ontime-ghosted': { ...ontimeInputGhosted },
|
'ontime-ghosted': { ...ontimeInputGhosted },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Kbd: {
|
||||||
|
baseStyle: {
|
||||||
|
borderRadius: '2px',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.20)',
|
||||||
|
background: 'rgba(255, 255, 255, 0.10)',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
variants: {
|
variants: {
|
||||||
'ontime-on-dark': { ...ontimeMenuOnDark },
|
'ontime-on-dark': { ...ontimeMenuOnDark },
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export {
|
|||||||
getFirstNormal,
|
getFirstNormal,
|
||||||
getLastEvent,
|
getLastEvent,
|
||||||
getLastEventNormal,
|
getLastEventNormal,
|
||||||
|
getLastNormal,
|
||||||
getNext,
|
getNext,
|
||||||
getNextEvent,
|
getNextEvent,
|
||||||
getNextEventNormal,
|
getNextEventNormal,
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import type { OntimeEvent, OntimeRundown } from 'ontime-types';
|
import type { NormalisedRundown, OntimeEvent, OntimeRundown } from 'ontime-types';
|
||||||
import { SupportedEvent } from 'ontime-types';
|
import { SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { getLastEvent, getNext, getNextEvent, getPrevious, getPreviousEvent, swapEventData } from './rundownUtils';
|
import {
|
||||||
|
getLastEvent,
|
||||||
|
getLastNormal,
|
||||||
|
getNext,
|
||||||
|
getNextEvent,
|
||||||
|
getPrevious,
|
||||||
|
getPreviousEvent,
|
||||||
|
swapEventData,
|
||||||
|
} from './rundownUtils';
|
||||||
|
|
||||||
describe('getNext()', () => {
|
describe('getNext()', () => {
|
||||||
it('returns the next event of type event', () => {
|
it('returns the next event of type event', () => {
|
||||||
@@ -85,9 +93,9 @@ describe('getPrevious()', () => {
|
|||||||
{ id: '3', type: SupportedEvent.Event },
|
{ id: '3', type: SupportedEvent.Event },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '3');
|
const { entry, index } = getPrevious(testRundown as OntimeRundown, '3');
|
||||||
expect(previousEvent?.id).toBe('2');
|
expect(entry?.id).toBe('2');
|
||||||
expect(previousIndex).toBe(1);
|
expect(index).toBe(1);
|
||||||
});
|
});
|
||||||
it('allow other event types', () => {
|
it('allow other event types', () => {
|
||||||
const testRundown = [
|
const testRundown = [
|
||||||
@@ -97,9 +105,9 @@ describe('getPrevious()', () => {
|
|||||||
{ id: '4', type: SupportedEvent.Event },
|
{ id: '4', type: SupportedEvent.Event },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '3');
|
const { entry, index } = getPrevious(testRundown as OntimeRundown, '3');
|
||||||
expect(previousEvent?.id).toBe('2');
|
expect(entry?.id).toBe('2');
|
||||||
expect(previousIndex).toBe(1);
|
expect(index).toBe(1);
|
||||||
});
|
});
|
||||||
it('returns null if none found', () => {
|
it('returns null if none found', () => {
|
||||||
const testRundown = [
|
const testRundown = [
|
||||||
@@ -108,9 +116,9 @@ describe('getPrevious()', () => {
|
|||||||
{ id: '4', type: SupportedEvent.Event },
|
{ id: '4', type: SupportedEvent.Event },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '2');
|
const { entry, index } = getPrevious(testRundown as OntimeRundown, '2');
|
||||||
expect(previousEvent).toBe(null);
|
expect(entry).toBe(null);
|
||||||
expect(previousIndex).toBe(null);
|
expect(index).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,4 +217,49 @@ describe('getLastEvent', () => {
|
|||||||
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
|
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
|
||||||
expect(lastEvent?.id).toBe('1');
|
expect(lastEvent?.id).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getLastNormal', () => {
|
||||||
|
it('returns the last entry', () => {
|
||||||
|
const testRundown = {
|
||||||
|
4: { id: '4', type: SupportedEvent.Block },
|
||||||
|
1: { id: '1', type: SupportedEvent.Event },
|
||||||
|
3: { id: '3', type: SupportedEvent.Event },
|
||||||
|
2: { id: '2', type: SupportedEvent.Delay },
|
||||||
|
};
|
||||||
|
|
||||||
|
const order = ['1', '2', '3', '4'];
|
||||||
|
|
||||||
|
const lastEntry = getLastNormal(testRundown as unknown as NormalisedRundown, order);
|
||||||
|
expect(lastEntry?.id).toBe('4');
|
||||||
|
});
|
||||||
|
it('handles rundowns with a single event', () => {
|
||||||
|
const testRundown = [{ id: '1', type: SupportedEvent.Event }];
|
||||||
|
|
||||||
|
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
|
||||||
|
expect(lastEvent?.id).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty order', () => {
|
||||||
|
const testRundown = {
|
||||||
|
4: { id: '4', type: SupportedEvent.Block },
|
||||||
|
1: { id: '1', type: SupportedEvent.Event },
|
||||||
|
3: { id: '3', type: SupportedEvent.Event },
|
||||||
|
2: { id: '2', type: SupportedEvent.Delay },
|
||||||
|
};
|
||||||
|
|
||||||
|
const order: string[] = [];
|
||||||
|
|
||||||
|
const lastEntry = getLastNormal(testRundown as unknown as NormalisedRundown, order);
|
||||||
|
expect(lastEntry).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty rundown', () => {
|
||||||
|
const testRundown = {};
|
||||||
|
|
||||||
|
const order = ['1', '2', '3', '4'];
|
||||||
|
|
||||||
|
const lastEntry = getLastNormal(testRundown as unknown as NormalisedRundown, order);
|
||||||
|
expect(lastEntry).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { NormalisedRundown, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import type { NormalisedRundown, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
import { isOntimeEvent } from 'ontime-types';
|
import { isOntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
type IndexAndEntry = { entry: OntimeRundownEntry | null; index: number | null };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets first event in rundown, if it exists
|
* Gets first event in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
@@ -62,6 +64,20 @@ export function getFirstEventNormal(
|
|||||||
return { firstEvent: null, firstIndex: null };
|
return { firstEvent: null, firstIndex: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets last event in a normalised rundown, if it exists
|
||||||
|
* @param rundown
|
||||||
|
* @param order
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getLastNormal(rundown: NormalisedRundown, order: string[]): OntimeRundownEntry | null {
|
||||||
|
const lastId = order.at(-1);
|
||||||
|
if (lastId === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return rundown[lastId] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets last scheduled event in rundown, if it exists
|
* Gets last scheduled event in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
@@ -138,19 +154,15 @@ export function getNext(
|
|||||||
* @param currentId
|
* @param currentId
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getNextNormal(
|
export function getNextNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
|
||||||
rundown: NormalisedRundown,
|
const currentIndex = order.findIndex((id) => id === currentId);
|
||||||
order: string[],
|
if (currentIndex !== -1 && currentIndex + 1 < order.length) {
|
||||||
currentId: string,
|
const index = currentIndex + 1;
|
||||||
): { nextEvent: OntimeRundownEntry | null; nextIndex: number | null } {
|
const nextId = order[index];
|
||||||
const index = order.findIndex((id) => id === currentId);
|
const entry = rundown[nextId];
|
||||||
if (index !== -1 && index + 1 < order.length) {
|
return { entry, index };
|
||||||
const nextIndex = index + 1;
|
|
||||||
const nextId = order[nextIndex];
|
|
||||||
const nextEvent = rundown[nextId];
|
|
||||||
return { nextEvent, nextIndex };
|
|
||||||
} else {
|
} else {
|
||||||
return { nextEvent: null, nextIndex: null };
|
return { entry: null, index: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,19 +221,15 @@ export function getNextEventNormal(
|
|||||||
* Gets previous entry in rundown, if it exists
|
* Gets previous entry in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
* @param {string} currentId
|
* @param {string} currentId
|
||||||
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
|
||||||
*/
|
*/
|
||||||
export function getPrevious(
|
export function getPrevious(rundown: OntimeRundownEntry[], currentId: string): IndexAndEntry {
|
||||||
rundown: OntimeRundownEntry[],
|
const currentIndex = rundown.findIndex((event) => event.id === currentId);
|
||||||
currentId: string,
|
if (currentIndex !== -1 && currentIndex - 1 >= 0) {
|
||||||
): { previousEvent: OntimeRundownEntry | null; previousIndex: number | null } {
|
const index = currentIndex - 1;
|
||||||
const index = rundown.findIndex((event) => event.id === currentId);
|
const entry = rundown[index];
|
||||||
if (index !== -1 && index - 1 >= 0) {
|
return { entry, index };
|
||||||
const previousIndex = index - 1;
|
|
||||||
const previousEvent = rundown[previousIndex];
|
|
||||||
return { previousEvent, previousIndex };
|
|
||||||
} else {
|
} else {
|
||||||
return { previousEvent: null, previousIndex: null };
|
return { entry: null, index: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,21 +238,16 @@ export function getPrevious(
|
|||||||
* @param rundown
|
* @param rundown
|
||||||
* @param order
|
* @param order
|
||||||
* @param {string} currentId
|
* @param {string} currentId
|
||||||
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
|
||||||
*/
|
*/
|
||||||
export function getPreviousNormal(
|
export function getPreviousNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
|
||||||
rundown: NormalisedRundown,
|
const currentIndex = order.findIndex((id) => id === currentId);
|
||||||
order: string[],
|
if (currentIndex !== -1 && currentIndex - 1 >= 0) {
|
||||||
currentId: string,
|
const index = currentIndex - 1;
|
||||||
): { previousEvent: OntimeRundownEntry | null; previousIndex: number | null } {
|
const previousId = order[index];
|
||||||
const index = order.findIndex((id) => id === currentId);
|
const entry = rundown[previousId];
|
||||||
if (index !== -1 && index - 1 >= 0) {
|
return { entry, index };
|
||||||
const previousIndex = index - 1;
|
|
||||||
const previousId = order[previousIndex];
|
|
||||||
const previousEvent = rundown[previousId];
|
|
||||||
return { previousEvent, previousIndex };
|
|
||||||
} else {
|
} else {
|
||||||
return { previousEvent: null, previousIndex: null };
|
return { entry: null, index: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user