Swap Event Data (#446)

* use zustand store in `ContextMenuContext`

* add `withDivider` prop to context menu `Option`

* add `isDisabled` prop to `Option`

* create `eventIdSwapping` store

* create swapping context menu options

* fix `key` in ContextMenu

* address `tanstack-eslint` errors

* create initial `requestEventSwap` event

* create initial `swapEvents` action

* use `swapEvents` in `EventBlock`

* move from `emitError` to `logAxiosError`

* remove extra curly brace from Copy ID

* add `clearEventId` func

* finalize context menu swapping logic

* write optimistic swapping logic

* remove unused import and type out handler in `rundownController`

* create `rundownSwapValidator`

* move index increase to `EventBlock`

* move swapping logic from `id` to `index`

* create `swapEvents` endpoint

* move `useEventIdSwapping` hook into its own file

* revert to using event `id`

* logic now uses indexes to swap events

* add `todo` to `swapEvents`

* revert index increment

* create `swapOntimeEvents` and export in `index.ts`

* use `swapOntimeEvents` in frontend & server

* update import path

* remove extra `setCached`
This commit is contained in:
asharonbaltazar
2023-08-15 16:04:53 -04:00
committed by GitHub
parent a46a0e1631
commit 51c31adaf1
17 changed files with 363 additions and 123 deletions
+12 -12
View File
@@ -4,9 +4,9 @@ import { ChakraProvider } from '@chakra-ui/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ContextMenu } from './common/components/context-menu/ContextMenu';
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
import { AppContextProvider } from './common/context/AppContext';
import { ContextMenuProvider } from './common/context/ContextMenuContext';
import useElectronEvent from './common/hooks/useElectronEvent';
import { ontimeQueryClient } from './common/queryClient';
import { socketClientName } from './common/stores/connectionName';
@@ -52,18 +52,18 @@ function App() {
<ChakraProvider resetCSS theme={theme}>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<ContextMenuProvider>
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<TranslationProvider>
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<TranslationProvider>
<ContextMenu>
<AppRouter />
</TranslationProvider>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</BrowserRouter>
</ContextMenuProvider>
</ContextMenu>
</TranslationProvider>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</BrowserRouter>
</AppContextProvider>
</QueryClientProvider>
</ChakraProvider>
+13
View File
@@ -50,6 +50,19 @@ export async function requestApplyDelay(eventId: string) {
return axios.patch(`${rundownURL}/applydelay/${eventId}`);
}
export type SwapEntry = {
from: string;
to: string;
};
/**
* @description HTTP request to swap two events
* @return {Promise}
*/
export async function requestEventSwap(data: SwapEntry) {
return axios.patch(`${rundownURL}/swap`, data);
}
/**
* @description HTTP request to delete given event
* @return {Promise}
@@ -0,0 +1,84 @@
// logic (with some modifications) culled from:
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
import { Fragment, ReactElement } from 'react';
import { Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
import { IconType } from '@react-icons/all-files';
import { create } from 'zustand';
import style from './ContextMenu.module.scss';
type ContextMenuCoords = {
x: number;
y: number;
};
export type Option = {
label: string;
icon: IconType;
onClick: () => void;
withDivider?: boolean;
isDisabled?: boolean;
};
type ContextMenuStore = {
coords: ContextMenuCoords;
options: Option[];
isOpen: boolean;
setContextMenu: (coords: ContextMenuCoords, options: Option[]) => void;
setIsOpen: (newIsOpen: boolean) => void;
};
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
coords: { x: 0, y: 0 },
options: [],
isOpen: false,
setContextMenu: (coords, options) => set(() => ({ coords, options, isOpen: true })),
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
}));
interface ContextMenuProps {
// ReactElement type required due to early `return` (line 51) returning {children}
children: ReactElement;
}
export const ContextMenu = ({ children }: ContextMenuProps) => {
const { coords, options, isOpen, setIsOpen } = useContextMenuStore();
const onClose = () => {
return setIsOpen(false);
};
if (!isOpen) {
return children;
}
return (
<>
{children}
<div className={style.contextMenuBackdrop} />
<Menu isOpen gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<MenuButton
className={style.contextMenuButton}
aria-hidden
w={1}
h={1}
style={{
left: coords.x,
top: coords.y,
}}
/>
<MenuList>
{options.map(({ label, icon: Icon, onClick, withDivider, isDisabled }, i) => (
<Fragment key={label}>
{withDivider && <MenuDivider />}
<MenuItem key={i} icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
{label}
</MenuItem>
</Fragment>
))}
</MenuList>
</Menu>
</>
);
};
@@ -1,76 +0,0 @@
// logic (with some modifications) culled from:
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
import { createContext, ReactNode, useState } from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
import { IconType } from '@react-icons/all-files';
import style from './ContextMenuContext.module.scss';
type ContextMenuCoords = {
x: number;
y: number;
};
type ContextMenuContextType = {
createContextMenu: (options: Option[], menuCoordinates: ContextMenuCoords) => void;
};
export const ContextMenuContext = createContext<ContextMenuContextType | null>(null);
export type Option = {
label: string;
icon: IconType;
onClick: () => void;
};
interface ContextMenuProviderProps {
children: ReactNode;
}
export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
const [isOpen, setIsOpen] = useState(false);
const [coords, setCoords] = useState<ContextMenuCoords>({ x: 0, y: 0 });
const [options, setOptions] = useState<Option[]>([]);
const onClose = () => {
return setIsOpen(false);
};
const createContextMenu = (options: Option[], menuCoords: ContextMenuCoords) => {
setCoords(menuCoords);
setOptions(options);
setIsOpen(true);
};
return (
<ContextMenuContext.Provider value={{ createContextMenu }}>
{children}
{isOpen && (
<>
<div className={style.contextMenuBackdrop} />
<Menu isOpen gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<MenuButton
className={style.contextMenuButton}
aria-hidden
w={1}
h={1}
style={{
left: coords.x,
top: coords.y,
}}
/>
<MenuList>
{options.map(({ label, icon: Icon, onClick }, i) => (
<MenuItem key={i} icon={<Icon />} onClick={onClick}>
{label}
</MenuItem>
))}
</MenuList>
</Menu>
</>
)}
</ContextMenuContext.Provider>
);
};
@@ -1,22 +1,16 @@
import { MouseEvent, useContext } from 'react';
import { MouseEvent } from 'react';
import { ContextMenuContext, Option } from '../context/ContextMenuContext';
import { Option, useContextMenuStore } from '../components/context-menu/ContextMenu';
export const useContextMenu = <T extends HTMLElement>(options: Option[]) => {
const contextMenuContext = useContext(ContextMenuContext);
if (contextMenuContext === null) {
throw new Error('useContextMenu should be wrapped by ContextMenuProvider');
}
const { createContextMenu } = contextMenuContext;
const { setContextMenu } = useContextMenuStore();
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
// prevent browser default context menu from showing up
contextMenuEvent.preventDefault();
const { pageX, pageY } = contextMenuEvent;
return createContextMenu(options, { x: pageX, y: pageY });
return setContextMenu({ x: pageX, y: pageY }, options);
};
return [localCreateContextMenu];
+79 -8
View File
@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
import { swapOntimeEvents } from 'ontime-utils';
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
import { logAxiosError } from '../api/apiUtils';
@@ -9,9 +10,11 @@ import {
requestApplyDelay,
requestDelete,
requestDeleteAll,
requestEventSwap,
requestPostEvent,
requestPutEvent,
requestReorderEvent,
SwapEntry,
} from '../api/eventsApi';
import { useEditorSettings } from '../stores/editorSettings';
@@ -28,9 +31,10 @@ export const useEventAction = () => {
* Calls mutation to add new event
* @private
*/
const _addEventMutation = useMutation(requestPostEvent, {
const _addEventMutation = useMutation({
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
mutationFn: requestPostEvent,
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
},
@@ -102,7 +106,8 @@ export const useEventAction = () => {
* Calls mutation to update existing event
* @private
*/
const _updateEventMutation = useMutation(requestPutEvent, {
const _updateEventMutation = useMutation({
mutationFn: requestPutEvent,
// we optimistically update here
onMutate: async (newEvent) => {
// cancel ongoing queries
@@ -117,11 +122,11 @@ export const useEventAction = () => {
// Return a context with the previous and new events
return { previousEvent, newEvent };
},
// Mutation fails, rollback undoes optimist update
onError: (_error, _newEvent, context) => {
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: async () => {
@@ -148,7 +153,8 @@ export const useEventAction = () => {
* Calls mutation to delete an event
* @private
*/
const _deleteEventMutation = useMutation(requestDelete, {
const _deleteEventMutation = useMutation({
mutationFn: requestDelete,
// we optimistically update here
onMutate: async (eventId) => {
// cancel ongoing queries
@@ -196,7 +202,8 @@ export const useEventAction = () => {
* Calls mutation to delete all events
* @private
*/
const _deleteAllEventsMutation = useMutation(requestDeleteAll, {
const _deleteAllEventsMutation = useMutation({
mutationFn: requestDeleteAll,
// we optimistically update here
onMutate: async () => {
// cancel ongoing queries
@@ -239,7 +246,8 @@ export const useEventAction = () => {
* Calls mutation to apply a delay
* @private
*/
const _applyDelayMutation = useMutation(requestApplyDelay, {
const _applyDelayMutation = useMutation({
mutationFn: requestApplyDelay,
// Mutation finished, failed or successful
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
@@ -265,7 +273,8 @@ export const useEventAction = () => {
* Calls mutation to reorder an event
* @private
*/
const _reorderEventMutation = useMutation(requestReorderEvent, {
const _reorderEventMutation = useMutation({
mutationFn: requestReorderEvent,
// we optimistically update here
onMutate: async (data) => {
// cancel ongoing queries
@@ -316,5 +325,67 @@ export const useEventAction = () => {
[_reorderEventMutation],
);
return { addEvent, updateEvent, deleteEvent, deleteAllEvents, applyDelay, reorderEvent };
/**
* Calls mutation to swap events
* @private
*/
const _swapEvents = useMutation({
mutationFn: requestEventSwap,
// we optimistically update here
onMutate: async ({ from, to }) => {
// cancel ongoing queries
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
// Snapshot the previous value
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
const fromEventIndex = rundown.findIndex((event) => event.id === from);
const toEventIndex = rundown.findIndex((event) => event.id === to);
const previousEvents = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
// optimistically update object
queryClient.setQueryData(RUNDOWN_TABLE, previousEvents);
// Return a context with the previous events
return { previousEvents };
},
// Mutation fails, rollback undoes optimist update
onError: (_error, _eventId, context) => {
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(RUNDOWN_TABLE);
},
networkMode: 'always',
});
/**
* Swaps the schedule of two events
*/
const swapEvents = useCallback(
async ({ from, to }: SwapEntry) => {
// TODO: before calling `/swapEvents`,
// we should determine the events are of type `OntimeEvent`
try {
await _swapEvents.mutateAsync({ from, to });
} catch (error) {
logAxiosError('Error re-ordering event', error);
}
},
[_swapEvents],
);
return {
addEvent,
updateEvent,
deleteEvent,
deleteAllEvents,
applyDelay,
reorderEvent,
swapEvents,
};
};
@@ -12,7 +12,7 @@ import BlockBlock from './block-block/BlockBlock';
import DelayBlock from './delay-block/DelayBlock';
import EventBlock from './event-block/EventBlock';
export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'delete' | 'clone' | 'update';
export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'delete' | 'clone' | 'update' | 'swap';
interface RundownEntryProps {
type: SupportedEvent;
@@ -44,7 +44,7 @@ export default function RundownEntry(props: RundownEntryProps) {
disableEdit,
} = props;
const { emitError } = useEmitLog();
const { addEvent, updateEvent, deleteEvent } = useEventAction();
const { addEvent, updateEvent, deleteEvent, swapEvents } = useEventAction();
const cursor = useAppMode((state) => state.cursor);
const setCursor = useAppMode((state) => state.setCursor);
@@ -95,6 +95,12 @@ export default function RundownEntry(props: RundownEntryProps) {
addEvent({ type: SupportedEvent.Block }, { after: data.id });
break;
}
case 'swap': {
const { value } = payload as FieldValue;
swapEvents({ from: value as string, to: data.id });
break;
}
case 'delete': {
if (openId === data.id) {
removeOpenEvent();
@@ -149,6 +155,7 @@ export default function RundownEntry(props: RundownEntryProps) {
removeOpenEvent,
startTimeIsLastEnd,
updateEvent,
swapEvents,
],
);
@@ -1,17 +1,19 @@
import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoCopyOutline } from '@react-icons/all-files/io5/IoCopyOutline';
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types';
import { useContextMenu } from '../../../common/hooks/useContextMenu';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { useAppMode } from '../../../common/stores/appModeStore';
import copyToClipboard from '../../../common/utils/copyToClipboard';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import type { EventItemActions } from '../RundownEntry';
import { useEventIdSwapping } from '../useEventIdSwapping';
import EventBlockInner from './EventBlockInner';
@@ -75,22 +77,37 @@ export default function EventBlock(props: EventBlockProps) {
actionHandler,
disableEdit,
} = props;
const { updateEvent } = useEventAction();
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
const moveCursorTo = useAppMode((state) => state.setCursor);
const handleRef = useRef<null | HTMLSpanElement>(null);
const [isVisible, setIsVisible] = useState(false);
const openId = useAppMode((state) => state.editId);
const [onContextMenu] = useContextMenu<HTMLDivElement>([
{ label: `Copy ID: ${eventId}}`, icon: IoCopyOutline, onClick: () => copyToClipboard(eventId) },
{ label: `Copy ID: ${eventId}`, icon: IoCopyOutline, onClick: () => copyToClipboard(eventId) },
{
label: 'Toggle public',
icon: IoPeopleOutline,
onClick: () =>
updateEvent({
id: eventId,
isPublic: !isPublic,
actionHandler('update', {
field: 'isPublic',
value: !isPublic,
}),
},
{
label: 'Add to swap',
icon: IoAdd,
onClick: () => setSelectedEventId(eventId),
withDivider: true,
},
{
label: `Swap this event with ${selectedEventId ?? ''}`,
icon: IoSwapVertical,
onClick: () => {
actionHandler('swap', { field: 'id', value: selectedEventId });
clearSelectedEventId();
},
isDisabled: selectedEventId == null || selectedEventId === eventId,
},
]);
const {
@@ -0,0 +1,13 @@
import { create } from 'zustand';
interface EventIdSwappingStore {
selectedEventId: string | null;
setSelectedEventId: (newEventId: string | null) => void;
clearSelectedEventId: () => void;
}
export const useEventIdSwapping = create<EventIdSwappingStore>((set) => ({
selectedEventId: null,
setSelectedEventId: (newEventId) => set(() => ({ selectedEventId: newEventId })),
clearSelectedEventId: () => set(() => ({ selectedEventId: null })),
}));
@@ -1,4 +1,3 @@
import { OntimeEvent } from 'ontime-types';
import { failEmptyObjects } from '../utils/routerUtils.js';
import {
addEvent,
@@ -7,19 +6,21 @@ import {
deleteEvent,
editEvent,
reorderEvent,
swapEvents,
} from '../services/rundown-service/RundownService.js';
import { getDelayedRundown } from '../services/rundown-service/delayedRundown.utils.js';
import { RequestHandler } from 'express';
// Create controller for GET request to '/events'
// Returns -
export const rundownGetAll = async (req, res) => {
export const rundownGetAll: RequestHandler = async (_req, res) => {
const delayedRundown = getDelayedRundown();
res.json(delayedRundown);
};
// Create controller for POST request to '/events/'
// Returns -
export const rundownPost = async (req, res) => {
export const rundownPost: RequestHandler = async (req, res) => {
if (failEmptyObjects(req.body, res)) {
return;
}
@@ -34,7 +35,7 @@ export const rundownPost = async (req, res) => {
// Create controller for PUT request to '/events/'
// Returns -
export const rundownPut = async (req, res) => {
export const rundownPut: RequestHandler = async (req, res) => {
if (failEmptyObjects(req.body, res)) {
return;
}
@@ -47,7 +48,7 @@ export const rundownPut = async (req, res) => {
}
};
export const rundownReorder = async (req, res) => {
export const rundownReorder: RequestHandler = async (req, res) => {
if (failEmptyObjects(req.body, res)) {
return;
}
@@ -61,9 +62,23 @@ export const rundownReorder = async (req, res) => {
}
};
export const rundownSwap: RequestHandler = async (req, res) => {
if (failEmptyObjects(req.body, res)) {
return;
}
try {
const { from, to } = req.body;
await swapEvents(from, to);
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for PATCH request to '/events/applydelay/:eventId'
// Returns -
export const rundownApplyDelay = async (req, res) => {
export const rundownApplyDelay: RequestHandler = async (req, res) => {
try {
await applyDelay(req.params.eventId);
res.sendStatus(200);
@@ -74,7 +89,7 @@ export const rundownApplyDelay = async (req, res) => {
// Create controller for DELETE request to '/events/:eventId'
// Returns -
export const deleteEventById = async (req, res) => {
export const deleteEventById: RequestHandler = async (req, res) => {
try {
await deleteEvent(req.params.eventId);
res.sendStatus(204);
@@ -85,7 +100,7 @@ export const deleteEventById = async (req, res) => {
// Create controller for DELETE request to '/events/'
// Returns -
export const rundownDelete = async (req, res) => {
export const rundownDelete: RequestHandler = async (req, res) => {
try {
await deleteAllEvents();
res.sendStatus(204);
@@ -29,6 +29,16 @@ export const rundownReorderValidator = [
},
];
export const rundownSwapValidator = [
body('from').isString().exists(),
body('to').isString().exists(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
export const paramsMustHaveEventId = [
param('eventId').exists(),
(req, res, next) => {
+4
View File
@@ -7,12 +7,14 @@ import {
rundownPost,
rundownPut,
rundownReorder,
rundownSwap,
} from '../controllers/rundownController.js';
import {
paramsMustHaveEventId,
rundownPostValidator,
rundownPutValidator,
rundownReorderValidator,
rundownSwapValidator,
} from '../controllers/rundownController.validate.js';
export const router = express.Router();
@@ -29,6 +31,8 @@ router.put('/', rundownPutValidator, rundownPut);
// create route between controller and '/events/reorder' endpoint
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
router.patch('/swap', rundownSwapValidator, rundownSwap);
// create route between controller and '/events/applydelay/:eventId' endpoint
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
@@ -22,6 +22,7 @@ import {
cachedDelete,
cachedEdit,
cachedReorder,
cachedSwap,
delayedRundownCacheKey,
} from './delayedRundown.utils.js';
import { logger } from '../../classes/Logger.js';
@@ -304,6 +305,22 @@ export function _applyDelay(
return { delayIndex, updatedRundown };
}
/**
* swaps two events
* @param {string} from - id of event from
* @param {string} to - id of event to
* @returns {Promise<void>}
*/
export async function swapEvents(from: string, to: string) {
await cachedSwap(from, to);
// notify timer service of changed events
updateTimer();
// advice socket subscribers of change
sendRefetch();
}
/**
* applies delay value for given event
* @param eventId
@@ -1,8 +1,10 @@
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
import { isProduction } from '../../setup.js';
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
import { swapOntimeEvents } from 'ontime-utils';
/**
* Key of rundown in cache
@@ -174,7 +176,33 @@ export async function cachedReorder(eventId: string, from: number, to: number) {
export async function cachedClear() {
await DataProvider.clearRundown();
runtimeCacheStore.setCached(delayedRundownCacheKey, []);
console.log(DataProvider.getRundown(), getDelayedRundown());
}
/**
* Swaps two events
* @param {string} fromEventId
* @param {string} toEventId
*/
export async function cachedSwap(fromEventId: string, toEventId: string) {
const fromEventIndex = DataProvider.getIndexOf(fromEventId);
const toEventIndex = DataProvider.getIndexOf(toEventId);
const rundown = DataProvider.getRundown();
const rundownToUpdate = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
const delayedRundown = getDelayedRundown();
const fromCachedEvent = delayedRundown.at(fromEventIndex);
const toCachedEvent = delayedRundown.at(toEventIndex);
if (fromCachedEvent.id !== fromEventId || toCachedEvent.id !== toEventId) {
// something went wrong, we invalidate the cache
runtimeCacheStore.invalidate(delayedRundownCacheKey);
} else {
const delayedRundownToUpdate = swapOntimeEvents(delayedRundown, fromEventIndex, toEventIndex);
runtimeCacheStore.setCached(delayedRundownCacheKey, delayedRundownToUpdate);
}
await DataProvider.setRundown(rundownToUpdate);
}
/**
+1
View File
@@ -4,6 +4,7 @@ export { validatePlayback } from './src/validate-action/validatePlayback.js';
// rundown utils
export { generateId } from './src/generate-id/generateId.js';
export { calculateDuration } from './src/rundown-utils/rundownUtils.js';
export { swapOntimeEvents } from './src/rundown-utils/rundownUtils.js';
// format utils
export { formatDisplay } from './src/date-utils/formatDisplay.js';
@@ -1,3 +1,5 @@
import { OntimeEvent, OntimeRundown } from 'ontime-types';
import { dayInMs } from '../timeConstants.js';
/**
@@ -13,3 +15,43 @@ export const calculateDuration = (timeStart: number, timeEnd: number): number =>
}
return timeEnd - timeStart;
};
/**
* @description swaps two OntimeEvents in the rundown
* @param {OntimeRundown} rundown
* @param {number} fromEventIndex
* @param {number} toEventIndex
* @returns {OntimeRundown}
*/
export const swapOntimeEvents = (
rundown: OntimeRundown,
fromEventIndex: number,
toEventIndex: number,
): OntimeRundown => {
const updatedRundown = [...rundown];
if (fromEventIndex < 0 || toEventIndex < 0) {
throw new Error('ID not found at index');
}
const fromEvent = updatedRundown.at(fromEventIndex) as OntimeEvent;
const toEvent = updatedRundown.at(toEventIndex) as OntimeEvent;
updatedRundown[fromEventIndex] = {
...toEvent,
timeStart: fromEvent.timeStart,
timeEnd: fromEvent.timeEnd,
duration: fromEvent.duration,
delay: fromEvent.delay,
};
updatedRundown[toEventIndex] = {
...fromEvent,
timeStart: toEvent.timeStart,
timeEnd: toEvent.timeEnd,
duration: toEvent.duration,
delay: toEvent.delay,
};
return updatedRundown;
};