refactor: delete several events

This commit is contained in:
Carlos Valente
2024-05-24 11:38:54 +02:00
committed by Carlos Valente
parent 16f31a07b7
commit a09aa922bb
14 changed files with 84 additions and 35 deletions
+2 -2
View File
@@ -74,8 +74,8 @@ export async function requestApplyDelay(eventId: string): Promise<AxiosResponse<
/** /**
* HTTP request to delete given event * HTTP request to delete given event
*/ */
export async function requestDelete(eventId: string): Promise<AxiosResponse<MessageResponse>> { export async function requestDelete(eventIds: string[]): Promise<AxiosResponse<MessageResponse>> {
return axios.delete(`${rundownPath}/${eventId}`); return axios.delete(rundownPath, { data: { ids: eventIds } });
} }
/** /**
@@ -337,7 +337,7 @@ export const useEventAction = () => {
const _deleteEventMutation = useMutation({ const _deleteEventMutation = useMutation({
mutationFn: requestDelete, mutationFn: requestDelete,
// we optimistically update here // we optimistically update here
onMutate: async (eventId) => { onMutate: async (eventIds: string[]) => {
// cancel ongoing queries // cancel ongoing queries
await queryClient.cancelQueries({ queryKey: RUNDOWN }); await queryClient.cancelQueries({ queryKey: RUNDOWN });
@@ -346,9 +346,11 @@ export const useEventAction = () => {
if (previousData) { if (previousData) {
// optimistically update object // optimistically update object
const newOrder = previousData.order.filter((id) => id !== eventId); const newOrder = previousData.order.filter((id) => !eventIds.includes(id));
const newRundown = { ...previousData.rundown }; const newRundown = { ...previousData.rundown };
delete newRundown[eventId]; for (const eventId of eventIds) {
delete newRundown[eventId];
}
queryClient.setQueryData(RUNDOWN, { queryClient.setQueryData(RUNDOWN, {
order: newOrder, order: newOrder,
@@ -377,9 +379,9 @@ export const useEventAction = () => {
* Deletes an event form the list * Deletes an event form the list
*/ */
const deleteEvent = useCallback( const deleteEvent = useCallback(
async (eventId: string) => { async (eventIds: string[]) => {
try { try {
await _deleteEventMutation.mutateAsync(eventId); await _deleteEventMutation.mutateAsync(eventIds);
} catch (error) { } catch (error) {
logAxiosError('Error deleting event', error); logAxiosError('Error deleting event', error);
} }
+1 -1
View File
@@ -45,7 +45,7 @@ export default function Rundown({ data }: RundownProps) {
(cursor: string | null) => { (cursor: string | null) => {
if (!cursor) return; if (!cursor) return;
const previous = getPreviousNormal(rundown, order, cursor).entry?.id ?? null; const previous = getPreviousNormal(rundown, order, cursor).entry?.id ?? null;
deleteEvent(cursor); deleteEvent([cursor]);
setCursor(previous); setCursor(previous);
}, },
[deleteEvent, order, rundown, setCursor], [deleteEvent, order, rundown, setCursor],
@@ -58,18 +58,20 @@ export default function RundownEntry(props: RundownEntryProps) {
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction(); const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
const cursor = useAppMode((state) => state.cursor); const cursor = useAppMode((state) => state.cursor);
const setCursor = useAppMode((state) => state.setCursor); const setCursor = useAppMode((state) => state.setCursor);
const { selectedEvents, clearSelectedEvents } = useEventSelection(); const { selectedEvents, unselect, clearSelectedEvents } = useEventSelection();
const removeOpenEvent = useCallback(() => { const removeOpenEvent = useCallback(() => {
if (selectedEvents.has(data.id)) { unselect(data.id);
clearSelectedEvents();
}
// clear cursor if we are deleting the event that is currently selected // clear cursor if we are deleting the event that is currently selected
if (cursor === data.id) { if (cursor === data.id) {
setCursor(null); setCursor(null);
} }
}, [selectedEvents, data.id, cursor, clearSelectedEvents, setCursor]); }, [unselect, data.id, cursor, setCursor]);
const clearMultiSelection = useCallback(() => {
clearSelectedEvents();
setCursor(null);
}, [clearSelectedEvents, setCursor]);
// Create / delete new events // Create / delete new events
type FieldValue = { type FieldValue = {
@@ -111,10 +113,12 @@ export default function RundownEntry(props: RundownEntryProps) {
return swapEvents({ from: value as string, to: data.id }); return swapEvents({ from: value as string, to: data.id });
} }
case 'delete': { case 'delete': {
if (selectedEvents.has(data.id)) { if (selectedEvents.size > 1) {
removeOpenEvent(); clearMultiSelection();
return deleteEvent(Array.from(selectedEvents));
} }
return deleteEvent(data.id); removeOpenEvent();
return deleteEvent([data.id]);
} }
case 'clone': { case 'clone': {
const newEvent = cloneEvent(data as OntimeEvent, data.id); const newEvent = cloneEvent(data as OntimeEvent, data.id);
@@ -134,7 +138,7 @@ export default function RundownEntry(props: RundownEntryProps) {
if (selectedEvents.size > 1) { if (selectedEvents.size > 1) {
const changes: Partial<OntimeEvent> = { [field]: value }; const changes: Partial<OntimeEvent> = { [field]: value };
batchUpdateEvents(changes, Array.from(selectedEvents)); batchUpdateEvents(changes, Array.from(selectedEvents));
return clearSelectedEvents(); return;
} }
if (field in data) { if (field in data) {
// @ts-expect-error -- not sure how to type this // @ts-expect-error -- not sure how to type this
@@ -50,7 +50,7 @@ export default function DelayBlock(props: DelayBlockProps) {
}; };
const cancelDelayHandler = () => { const cancelDelayHandler = () => {
deleteEvent(data.id); deleteEvent([data.id]);
}; };
const blockClasses = cx([style.delay, hasCursor ? style.hasCursor : null]); const blockClasses = cx([style.delay, hasCursor ? style.hasCursor : null]);
@@ -113,6 +113,7 @@ export default function EventBlock(props: EventBlockProps) {
value: false, value: false,
}), }),
}, },
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
] ]
: [ : [
{ {
@@ -13,6 +13,8 @@ interface EventSelectionStore {
anchoredIndex: number | null; anchoredIndex: number | null;
setSelectedEvents: (selectionArgs: { id: string; index: number; selectMode: SelectionMode }) => void; setSelectedEvents: (selectionArgs: { id: string; index: number; selectMode: SelectionMode }) => void;
clearSelectedEvents: () => void; clearSelectedEvents: () => void;
clearMultiSelect: () => void;
unselect: (id: string) => void;
} }
export const useEventSelection = create<EventSelectionStore>()((set, get) => ({ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
@@ -81,7 +83,17 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
}); });
} }
}, },
clearSelectedEvents: () => set({ selectedEvents: new Set() }), clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null }),
clearMultiSelect: () => {
const { selectedEvents } = get();
const [firstSelected] = selectedEvents;
set({ selectedEvents: new Set(firstSelected || undefined), anchoredIndex: null });
},
unselect: (id: string) => {
const { selectedEvents } = get();
selectedEvents.delete(id);
set({ selectedEvents });
},
})); }));
export function getSelectionMode(event: MouseEvent): SelectionMode { export function getSelectionMode(event: MouseEvent): SelectionMode {
@@ -119,10 +119,10 @@ export async function rundownDelete(_req: Request, res: Response<MessageResponse
} }
} }
export async function deleteEventById(req: Request, res: Response<MessageResponse | ErrorResponse>) { export async function deletesEventById(req: Request, res: Response<MessageResponse | ErrorResponse>) {
try { try {
await deleteEvent(req.params.eventId); await deleteEvent(req.body.ids);
res.status(204).send({ message: 'Event deleted' }); res.status(204).send({ message: 'Events deleted' });
} catch (error) { } catch (error) {
const message = getErrorMessage(error); const message = getErrorMessage(error);
res.status(400).send({ message }); res.status(400).send({ message });
@@ -1,7 +1,7 @@
import express from 'express'; import express from 'express';
import { import {
deleteEventById, deletesEventById,
rundownApplyDelay, rundownApplyDelay,
rundownBatchPut, rundownBatchPut,
rundownDelete, rundownDelete,
@@ -14,6 +14,7 @@ import {
} from './rundown.controller.js'; } from './rundown.controller.js';
import { import {
paramsMustHaveEventId, paramsMustHaveEventId,
rundownArrayOfIds,
rundownBatchPutValidator, rundownBatchPutValidator,
rundownPostValidator, rundownPostValidator,
rundownPutValidator, rundownPutValidator,
@@ -35,5 +36,5 @@ router.patch('/reorder/', rundownReorderValidator, rundownReorder);
router.patch('/swap', rundownSwapValidator, rundownSwap); router.patch('/swap', rundownSwapValidator, rundownSwap);
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay); router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
router.delete('/', rundownArrayOfIds, deletesEventById);
router.delete('/all', rundownDelete); router.delete('/all', rundownDelete);
router.delete('/:eventId', paramsMustHaveEventId, deleteEventById);
@@ -64,3 +64,14 @@ export const paramsMustHaveEventId = [
next(); next();
}, },
]; ];
export const rundownArrayOfIds = [
body('ids').isArray().exists(),
body('ids.*').isString(),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
@@ -90,9 +90,9 @@ export async function addEvent(eventData: PatchWithId & { after?: string }): Pro
* deletes event by its ID * deletes event by its ID
* @param eventId * @param eventId
*/ */
export async function deleteEvent(eventId: string) { export async function deleteEvent(eventIds: string[]) {
const scopedMutation = cache.mutateCache(cache.remove); const scopedMutation = cache.mutateCache(cache.remove);
const { didMutate } = await scopedMutation({ eventId }); const { didMutate } = await scopedMutation({ eventIds });
if (didMutate === false) { if (didMutate === false) {
return; return;
@@ -102,7 +102,7 @@ export async function deleteEvent(eventId: string) {
updateRuntimeOnChange(); updateRuntimeOnChange();
// notify timer and external services of change // notify timer and external services of change
notifyChanges({ timer: [eventId], external: true }); notifyChanges({ timer: eventIds, external: true });
} }
/** /**
@@ -347,9 +347,22 @@ describe('remove() mutation', () => {
test('deletes an event from the rundown', () => { test('deletes an event from the rundown', () => {
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent; const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
const testRundown: OntimeRundown = [mockEvent]; const testRundown: OntimeRundown = [mockEvent];
const { newRundown } = remove({ eventId: mockEvent.id, persistedRundown: testRundown }); const { newRundown } = remove({ eventIds: [mockEvent.id], persistedRundown: testRundown });
expect(newRundown.length).toBe(0); expect(newRundown.length).toBe(0);
}); });
test('deletes multiple events from the rundown', () => {
const testRundown: OntimeRundown = [
{ type: SupportedEvent.Event, id: '1' } as OntimeEvent,
{ type: SupportedEvent.Block, id: '2' } as OntimeBlock,
{ type: SupportedEvent.Delay, id: '3' } as OntimeDelay,
{ type: SupportedEvent.Event, id: '4' } as OntimeEvent,
{ type: SupportedEvent.Event, id: '5' } as OntimeEvent,
{ type: SupportedEvent.Event, id: '6' } as OntimeEvent,
];
const { newRundown } = remove({ eventIds: ['1', '2', '3'], persistedRundown: testRundown });
expect(newRundown.length).toBe(3);
expect(newRundown.at(0).id).toBe('4');
});
}); });
describe('edit() mutation', () => { describe('edit() mutation', () => {
@@ -9,7 +9,7 @@ import {
OntimeRundown, OntimeRundown,
OntimeRundownEntry, OntimeRundownEntry,
} from 'ontime-types'; } from 'ontime-types';
import { generateId, deleteAtIndex, insertAtIndex, reorderArray, swapEventData, checkIsNextDay } from 'ontime-utils'; import { generateId, insertAtIndex, reorderArray, swapEventData, checkIsNextDay } from 'ontime-utils';
import { DataProvider } from '../../classes/data-provider/DataProvider.js'; import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { createPatch } from '../../utils/parser.js'; import { createPatch } from '../../utils/parser.js';
@@ -263,13 +263,12 @@ export function add({ persistedRundown, atIndex, event }: AddArgs): Required<Mut
return { newRundown, newEvent, didMutate: true }; return { newRundown, newEvent, didMutate: true };
} }
type RemoveArgs = MutationParams<{ eventId: string }>; type RemoveArgs = MutationParams<{ eventIds: string[] }>;
export function remove({ persistedRundown, eventId }: RemoveArgs): MutatingReturn { export function remove({ persistedRundown, eventIds }: RemoveArgs): MutatingReturn {
const atIndex = persistedRundown.findIndex((event) => event.id === eventId); const newRundown = persistedRundown.filter((event) => !eventIds.includes(event.id));
const newRundown = deleteAtIndex(atIndex, persistedRundown);
return { newRundown, didMutate: atIndex !== -1 }; return { newRundown, didMutate: persistedRundown.length !== newRundown.length };
} }
export function removeAll(): MutatingReturn { export function removeAll(): MutatingReturn {
@@ -34,6 +34,12 @@ export function deleteAtIndex<T>(index: number, array: T[]) {
return array.filter((_, i) => i !== index); return array.filter((_, i) => i !== index);
} }
/**
* Reorders two objects in an array
* @param array
* @param fromIndex
* @param toIndex
*/
export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number) { export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number) {
if (fromIndex === toIndex) { if (fromIndex === toIndex) {
return array; // No change needed, return the original array return array; // No change needed, return the original array