diff --git a/client/src/app/api/eventsApi.js b/client/src/app/api/eventsApi.js index d55ede305..9ce2bba16 100644 --- a/client/src/app/api/eventsApi.js +++ b/client/src/app/api/eventsApi.js @@ -1,8 +1,29 @@ +import axios from 'axios'; import { serverURL } from './apiConstants'; -export const eventsURL = serverURL + 'events'; +export const eventsNamespace = 'events'; +export const eventsURL = serverURL + eventsNamespace; export const fetchAllEvents = async () => { - const res = await fetch(eventsURL); - // TODO: Safe json convert - return res.json(); -}; \ No newline at end of file + const res = await axios.get(eventsURL); + return res.data; +}; + +export const requestPost = async (data) => { + const res = await axios.post(eventsURL, data); + return res; +}; + +export const requestPut = async (data) => { + const res = await axios.put(eventsURL, data); + return res; +}; + +export const requestPatch = async (data) => { + const res = await axios.patch(eventsURL, data); + return res; +}; + +export const requestDelete = async (eventId) => { + const res = await axios.delete(eventsURL + '/' + eventId); + return res; +}; diff --git a/client/src/common/input/EditableTimer.jsx b/client/src/common/input/EditableTimer.jsx index 14273eafd..2fa772e9a 100644 --- a/client/src/common/input/EditableTimer.jsx +++ b/client/src/common/input/EditableTimer.jsx @@ -10,6 +10,7 @@ export default function EditableTimer(props) { // prepare time fields useEffect(() => { + if (time == null) return; setValue(addAndFormat(time, delay)); }, [time, delay]); diff --git a/client/src/features/editors/list/EventListWrapper.jsx b/client/src/features/editors/list/EventListWrapper.jsx index 5b8138330..3bcfc46e5 100644 --- a/client/src/features/editors/list/EventListWrapper.jsx +++ b/client/src/features/editors/list/EventListWrapper.jsx @@ -1,22 +1,121 @@ -import { useMutation, useQuery } from 'react-query'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useEffect } from 'react'; -import { fetchAllEvents } from '../../../app/api/eventsApi.js'; +import { + eventsNamespace, + fetchAllEvents, + requestPatch, + requestPost, + requestPut, + requestDelete, +} from '../../../app/api/eventsApi.js'; import EventList from './EventList'; import EventListMenu from '../../menu/EventListMenu.jsx'; -import axios from 'axios'; import { showErrorToast } from '../../../common/helpers/toastManager'; -import { eventsURL } from '../../../app/api/eventsApi'; import { Skeleton } from '@chakra-ui/skeleton'; import style from './List.module.css'; export default function EventListWrapper() { - const { data, status, isError, refetch } = useQuery('events', fetchAllEvents); - const addEvent = useMutation((data) => axios.post(eventsURL, data)); - const updateEvent = useMutation((data) => axios.put(eventsURL, data)); - const patchEvent = useMutation((data) => axios.patch(eventsURL, data)); - const deleteEvent = useMutation((eventId) => - axios.delete(eventsURL + '/' + eventId) + const queryClient = useQueryClient(); + const { data, status, isError, refetch } = useQuery( + eventsNamespace, + fetchAllEvents ); + const addEvent = useMutation(requestPost, { + // we optimistically update here + onMutate: async (newEvent) => { + // cancel ongoing queries + queryClient.cancelQueries(eventsNamespace); + + // Snapshot the previous value + const previousEvents = queryClient.getQueryData(eventsNamespace); + + // optimistically update object, temp ID until refetch + queryClient.setQueryData(eventsNamespace, (old) => [ + ...old, + { ...newEvent, id: new Date().toISOString() }, + ]); + + // Return a context with the previous and new todo + return { previousEvents }; + }, + + // Mutation fails, rollback undos optimist update + onError: (error, newEvent, context) => { + queryClient.setQueryData(eventsNamespace, context.previousEvents); + }, + // Mutation finished, failed or successful + // Fetch anyway, just to be sure + onSettled: () => { + queryClient.invalidateQueries(eventsNamespace); + }, + }); + const updateEvent = useMutation(requestPut, { + // we optimistically update here + onMutate: async (newEvent) => { + // cancel ongoing queries + queryClient.cancelQueries([eventsNamespace, newEvent.id]); + + // Snapshot the previous value + const previousEvent = queryClient.getQueryData([ + eventsNamespace, + newEvent.id, + ]); + + // optimistically update object + queryClient.setQueryData([eventsNamespace, 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( + [eventsNamespace, context.newEvent.id], + context.previousEvent + ); + }, + // Mutation finished, failed or successful + // Fetch anyway, just to be sure + onSettled: (newEvent) => { + queryClient.invalidateQueries([eventsNamespace, newEvent.id]); + }, + }); + + const patchEvent = useMutation(requestPatch, { + // we optimistically update here + onMutate: async (newEvent) => { + // cancel ongoing queries + queryClient.cancelQueries([eventsNamespace, newEvent.id]); + + // Snapshot the previous value + const previousEvent = queryClient.getQueryData([ + eventsNamespace, + newEvent.id, + ]); + + // optimistically update object + queryClient.setQueryData([eventsNamespace, 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( + [eventsNamespace, context.newEvent.id], + context.previousEvent + ); + }, + // Mutation finished, failed or successful + // Fetch anyway, just to be sure + onSettled: (newEvent) => { + queryClient.invalidateQueries([eventsNamespace, newEvent.id]); + }, + }); + + const deleteEvent = useMutation(requestDelete); // Show toasts on errors useEffect(() => { @@ -31,31 +130,29 @@ export default function EventListWrapper() { switch (action) { case 'add': try { - await addEvent.mutateAsync(payload).then((needsRefetch = true)); + await addEvent.mutateAsync(payload); } catch (error) { showErrorToast('Error creating event', error.message); } break; case 'update': try { - await updateEvent.mutateAsync(payload).then((needsRefetch = true)); - // TODO: instead of refetching, update the item here + await updateEvent.mutateAsync(payload); } catch (error) { showErrorToast('Error updating event', error.message); } break; case 'patch': try { - await patchEvent.mutateAsync(payload).then((needsRefetch = true)); - // TODO: instead of refetching, update the item here + await patchEvent.mutateAsync(payload); } catch (error) { showErrorToast('Error updating event', error.message); } break; case 'delete': + // TODO: could do optimistic update here? try { await deleteEvent.mutateAsync(payload).then((needsRefetch = true)); - needsRefetch = true; } catch (error) { showErrorToast('Error deleting event', error.message); } diff --git a/server/controllers/eventsController.js b/server/controllers/eventsController.js index 78052ea22..413db5537 100644 --- a/server/controllers/eventsController.js +++ b/server/controllers/eventsController.js @@ -113,7 +113,7 @@ exports.eventsPatch = async (req, res) => { const newEvents = replaceAt(events, itemIndex, updatedEvent); events = [...newEvents]; - res.sendStatus(200); + res.send(updatedEvent); }; // Create controller for DELETE request to '/events/' @@ -128,8 +128,7 @@ exports.eventsDelete = async (req, res) => { if (itemIndex === -1) { res.sendStatus(400); return; - } - else if (itemIndex === 0) events.shift(); + } else if (itemIndex === 0) events.shift(); else events.splice(itemIndex, 1); // Update events diff --git a/server/data/eventsDefinition.js b/server/data/eventsDefinition.js index e0d942acc..e050be5d9 100644 --- a/server/data/eventsDefinition.js +++ b/server/data/eventsDefinition.js @@ -3,8 +3,8 @@ const event = { title: '', subtitle: '', presenter: '', - timeStart: new Date(), - timeEnd: new Date(), + timeStart: null, + timeEnd: null, clockStarted: null, type: 'event', };