mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 04:13:47 +00:00
8c740b7111
* add dependency adding dependency to parse the excel file in the node side * import file ability to import file - add extension - safeguard file type and size * user is able to upload json or excel * chore: extract logic into self contained function * associate model with version * test id generation * parse excel data * prevent UI crash on bad data * extract validation into self contained function * chore: add more tests for bad data * config: stop from opening browser on start * fix comma in sample db * handle corrupt files * error boundary around main components * jsdocs * increase id length * parse excel time string * feat: excel parsing * validate json db before import * chore: test event validator * feat: make function to clean convert strings * chore: test parser * chore: jsdocs * fix: bug on upload prevent bug where the component would prevent upload of same file twice
348 lines
10 KiB
React
348 lines
10 KiB
React
import { useMutation, useQueryClient } from 'react-query';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
fetchAllEvents,
|
|
requestPatch,
|
|
requestPost,
|
|
requestPut,
|
|
requestDelete,
|
|
requestDeleteAll,
|
|
requestReorder,
|
|
requestApplyDelay,
|
|
} from 'app/api/eventsApi.js';
|
|
import EventList from './EventList';
|
|
import EventListMenu from 'features/menu/EventListMenu.jsx';
|
|
import { showErrorToast } from 'common/helpers/toastManager';
|
|
import { useFetch } from 'app/hooks/useFetch.js';
|
|
import Empty from 'common/state/Empty';
|
|
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
|
import { BatchOperation } from 'app/context/collapseAtom';
|
|
import { useAtom } from 'jotai';
|
|
|
|
export default function EventListWrapper() {
|
|
const [, setCollapsed] = useAtom(BatchOperation);
|
|
const queryClient = useQueryClient();
|
|
const { data, status, isError, refetch } = useFetch(
|
|
EVENTS_TABLE,
|
|
fetchAllEvents
|
|
);
|
|
const [events, setEvents] = useState(null);
|
|
|
|
const addEvent = useMutation(requestPost, {
|
|
// we optimistically update here
|
|
onMutate: async (newEvent) => {
|
|
// cancel ongoing queries
|
|
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
|
|
|
// Snapshot the previous value
|
|
let previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
|
if (previousEvents == null) {
|
|
refetch();
|
|
previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
|
}
|
|
|
|
// optimistically update object, temp ID until refetch
|
|
let optimistic = [...previousEvents];
|
|
optimistic.splice(newEvent.order, 0, {
|
|
...newEvent,
|
|
id: new Date().toISOString(),
|
|
});
|
|
queryClient.setQueryData(EVENTS_TABLE, optimistic);
|
|
|
|
// Return a context with the previous and new todo
|
|
return { previousEvents };
|
|
},
|
|
|
|
// Mutation fails, rollback undos optimist update
|
|
onError: (error, newEvent, context) => {
|
|
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
|
},
|
|
// Mutation finished, failed or successful
|
|
// Fetch anyway, just to be sure
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries(EVENTS_TABLE);
|
|
},
|
|
});
|
|
|
|
const updateEvent = useMutation(requestPut, {
|
|
// we optimistically update here
|
|
onMutate: async (newEvent) => {
|
|
// cancel ongoing queries
|
|
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
|
|
|
// Snapshot the previous value
|
|
const previousEvent = queryClient.getQueryData([
|
|
EVENTS_TABLE,
|
|
newEvent.id,
|
|
]);
|
|
|
|
// optimistically update object
|
|
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
|
|
|
// Return a context with the previous and new todo
|
|
return { previousEvent, newEvent };
|
|
},
|
|
|
|
// Mutation fails, rollback undos optimist update
|
|
onError: (error, newEvent, context) => {
|
|
queryClient.setQueryData(
|
|
[EVENTS_TABLE, context.newEvent.id],
|
|
context.previousEvent
|
|
);
|
|
},
|
|
// Mutation finished, failed or successful
|
|
// Fetch anyway, just to be sure
|
|
onSettled: (newEvent) => {
|
|
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
|
},
|
|
});
|
|
|
|
const patchEvent = useMutation(requestPatch, {
|
|
// we optimistically update here
|
|
onMutate: async (newEvent) => {
|
|
// cancel ongoing queries
|
|
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
|
|
|
// Snapshot the previous value
|
|
const previousEvent = queryClient.getQueryData([
|
|
EVENTS_TABLE,
|
|
newEvent.id,
|
|
]);
|
|
|
|
// optimistically update object
|
|
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
|
|
|
// Return a context with the previous and new todo
|
|
return { previousEvent, newEvent };
|
|
},
|
|
|
|
// Mutation fails, rollback undos optimist update
|
|
onError: (error, newEvent, context) => {
|
|
queryClient.setQueryData(
|
|
[EVENTS_TABLE, context.newEvent.id],
|
|
context.previousEvent
|
|
);
|
|
},
|
|
// Mutation finished, failed or successful
|
|
// Fetch anyway, just to be sure
|
|
onSettled: (newEvent) => {
|
|
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
|
},
|
|
});
|
|
|
|
const deleteEvent = useMutation(requestDelete, {
|
|
// we optimistically update here
|
|
onMutate: async (eventId) => {
|
|
// cancel ongoing queries
|
|
queryClient.cancelQueries([EVENTS_TABLE, eventId]);
|
|
|
|
// Snapshot the previous value
|
|
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
|
|
|
let filtered = [...previousEvents];
|
|
filtered.filter((e) => e.id === 'eventId');
|
|
|
|
// optimistically update object
|
|
queryClient.setQueryData(EVENTS_TABLE, filtered);
|
|
|
|
// Return a context with the previous and new todo
|
|
return { previousEvents };
|
|
},
|
|
|
|
// Mutation fails, rollback undos optimist update
|
|
onError: (error, eventId, context) => {
|
|
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
|
},
|
|
// Mutation finished, failed or successful
|
|
// Fetch anyway, just to be sure
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries(EVENTS_TABLE);
|
|
},
|
|
});
|
|
|
|
const deleteAllEvents = useMutation(requestDeleteAll, {
|
|
// we optimistically update here
|
|
onMutate: async () => {
|
|
// cancel ongoing queries
|
|
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
|
|
|
// Snapshot the previous value
|
|
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
|
|
|
let clear = [];
|
|
|
|
// optimistically update object
|
|
queryClient.setQueryData(EVENTS_TABLE, clear);
|
|
|
|
// Return a context with the previous and new todo
|
|
return { previousEvents };
|
|
},
|
|
|
|
// Mutation fails, rollback undos optimist update
|
|
onError: (error, eventId, context) => {
|
|
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
|
},
|
|
// Mutation finished, failed or successful
|
|
// Fetch anyway, just to be sure
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries(EVENTS_TABLE);
|
|
},
|
|
});
|
|
|
|
const applyDelay = useMutation(requestApplyDelay, {
|
|
// Mutation finished, failed or successful
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries(EVENTS_TABLE);
|
|
},
|
|
});
|
|
|
|
const reorderEvent = useMutation(requestReorder, {
|
|
// we optimistically update here
|
|
onMutate: async (data) => {
|
|
// cancel ongoing queries
|
|
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
|
|
|
// Snapshot the previous value
|
|
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
|
|
|
const e = [...previousEvents];
|
|
const [reorderedItem] = e.splice(data.from, 1);
|
|
e.splice(data.to, 0, reorderedItem);
|
|
|
|
// optimistically update object
|
|
queryClient.setQueryData(EVENTS_TABLE, e);
|
|
|
|
// Return a context with the previous and new todo
|
|
return { previousEvents };
|
|
},
|
|
|
|
// Mutation fails, rollback undos optimist update
|
|
onError: (error, eventId, context) => {
|
|
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
|
},
|
|
// Mutation finished, failed or successful
|
|
// Fetch anyway, just to be sure
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries(EVENTS_TABLE);
|
|
},
|
|
});
|
|
|
|
// Show toasts on errors
|
|
useEffect(() => {
|
|
if (isError) {
|
|
showErrorToast('Error fetching data');
|
|
}
|
|
}, [isError]);
|
|
|
|
// Events API
|
|
const eventsHandler = useCallback(
|
|
async (action, payload) => {
|
|
switch (action) {
|
|
case 'add':
|
|
try {
|
|
await addEvent.mutateAsync(payload);
|
|
} catch (error) {
|
|
showErrorToast('Error creating event', error.message);
|
|
}
|
|
break;
|
|
case 'update':
|
|
try {
|
|
await updateEvent.mutateAsync(payload);
|
|
} catch (error) {
|
|
showErrorToast('Error updating event', error.message);
|
|
}
|
|
break;
|
|
case 'patch':
|
|
try {
|
|
await patchEvent.mutateAsync(payload);
|
|
} catch (error) {
|
|
showErrorToast('Error updating event', error.message);
|
|
}
|
|
break;
|
|
case 'delete':
|
|
try {
|
|
await deleteEvent.mutateAsync(payload);
|
|
} catch (error) {
|
|
showErrorToast('Error deleting event', error.message);
|
|
}
|
|
break;
|
|
case 'reorder':
|
|
try {
|
|
await reorderEvent.mutateAsync(payload);
|
|
} catch (error) {
|
|
showErrorToast('Error reordering event', error.message);
|
|
}
|
|
break;
|
|
case 'applyDelay':
|
|
// if delay <= 0 delete delay and next block
|
|
if (payload.duration <= 0) {
|
|
try {
|
|
// look for block after
|
|
let afterId = false;
|
|
let blockAfter = null;
|
|
for (const d of data) {
|
|
if (d.id === payload.id) afterId = true;
|
|
if (afterId && d.type === 'block') {
|
|
blockAfter = d.id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// delete delay
|
|
await deleteEvent.mutateAsync(payload.id);
|
|
// delete block after, if any
|
|
if (blockAfter) await deleteEvent.mutateAsync(blockAfter);
|
|
} catch (error) {
|
|
showErrorToast('Error applying delay', error.message);
|
|
}
|
|
} else {
|
|
try {
|
|
await applyDelay.mutateAsync(payload.id);
|
|
} catch (error) {
|
|
showErrorToast('Error applying delay', error.message);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'collapseall':
|
|
if (data == null) return;
|
|
setCollapsed({ clear: true, items: data, isCollapsed: true });
|
|
break;
|
|
case 'expandall':
|
|
if (data == null) return;
|
|
setCollapsed({ clear: true, items: data, isCollapsed: false });
|
|
break;
|
|
|
|
case 'deleteall':
|
|
try {
|
|
await deleteAllEvents.mutateAsync();
|
|
} catch (error) {
|
|
showErrorToast('Error deleting events', error.message);
|
|
}
|
|
break;
|
|
default:
|
|
showErrorToast('Unrecognised request', action);
|
|
break;
|
|
}
|
|
},
|
|
[data]
|
|
);
|
|
|
|
// Front end should handle bad arguments
|
|
useEffect(() => {
|
|
if (data == null) return;
|
|
setEvents(data.filter((d) => Object.keys(d).length > 0));
|
|
}, [data]);
|
|
|
|
return (
|
|
<>
|
|
<EventListMenu eventsHandler={eventsHandler} />
|
|
{status === 'success' && events != null ? (
|
|
<EventList events={events} eventsHandler={eventsHandler} />
|
|
) : (
|
|
<Empty text='Connecting to server' />
|
|
)}
|
|
</>
|
|
);
|
|
}
|