diff --git a/packages/core/src/types/event.types.ts b/packages/core/src/types/event.types.ts index f0bbde16..48608e3b 100644 --- a/packages/core/src/types/event.types.ts +++ b/packages/core/src/types/event.types.ts @@ -56,6 +56,7 @@ export interface Schema_Event { title?: string; updatedAt?: Date; user?: string; + optimistic?: boolean; } export interface Query_Event extends Query { diff --git a/packages/web/src/common/utils/event.util.ts b/packages/web/src/common/utils/event.util.ts index 60468858..d89c0aef 100644 --- a/packages/web/src/common/utils/event.util.ts +++ b/packages/web/src/common/utils/event.util.ts @@ -1,5 +1,6 @@ import { schema } from "normalizr"; import dayjs, { Dayjs } from "dayjs"; +import { v4 as uuidv4 } from "uuid"; import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; import isBetween from "dayjs/plugin/isBetween"; @@ -209,3 +210,13 @@ export const prepEvtBeforeSubmit = (draft: Schema_GridEvent) => { export const normalizedEventsSchema = () => new schema.Entity("events", {}, { idAttribute: "_id" }); + +export const createOptimisticEvent = (event: Schema_Event) => { + const _event = { + ...event, + _id: `optimistic-${uuidv4()}`, + optimistic: true, + } as Schema_Event; + + return _event; +}; diff --git a/packages/web/src/ducks/events/event.types.ts b/packages/web/src/ducks/events/event.types.ts index 58f1a8b9..5d97a654 100644 --- a/packages/web/src/ducks/events/event.types.ts +++ b/packages/web/src/ducks/events/event.types.ts @@ -45,6 +45,10 @@ export interface Action_InsertEvents extends Action { payload: Entities_Event | undefined; } +export interface Action_ReplaceEvent extends Action { + payload: Payload_ReplaceEvent; +} + export interface Action_TimezoneChange extends Action { payload: { timezone: string }; } @@ -80,6 +84,11 @@ export interface Payload_EditEvent { shouldRemove?: boolean; } +export interface Payload_ReplaceEvent { + oldEventId: string; + newEvent: Schema_Event; +} + export interface Payload_GetPaginatedEvents extends Filters_Pagination { priorities: Priorities[]; } diff --git a/packages/web/src/ducks/events/sagas/event.sagas.ts b/packages/web/src/ducks/events/sagas/event.sagas.ts index 4765559a..91aa7b84 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.ts @@ -10,6 +10,7 @@ import { EventApi } from "@web/ducks/events/event.api"; import { selectEventById } from "@web/ducks/events/selectors/event.selectors"; import { selectPaginatedEventsBySectionType } from "@web/ducks/events/selectors/util.selectors"; import { + createOptimisticEvent, handleError, normalizedEventsSchema, } from "@web/common/utils/event.util"; @@ -112,38 +113,74 @@ function* convertTimedEvent({ payload }: Action_ConvertTimedEvent) { } function* createEvent({ payload }: Action_CreateEvent) { + const event = createOptimisticEvent(payload); try { + // Insert the optimistic event into the store to immediately display the event + if (payload.isSomeday) { + yield put(getSomedayEventsSlice.actions.insert(event._id)); + } else { + yield put(getWeekEventsSlice.actions.insert(event._id)); + } + yield put( + eventsEntitiesSlice.actions.insert( + normalize(event, normalizedEventsSchema()).entities.events + ) + ); + + // Send a request to create the event const res = (yield call( EventApi.create, payload )) as Response_CreateEventSaga; - const normalizedEvent = normalize( - res.data, - normalizedEventsSchema() - ); - + // Replace the optimistic event with the actual event if (payload.isSomeday) { - yield put(getSomedayEventsSlice.actions.insert(res.data._id)); + yield put( + getSomedayEventsSlice.actions.replace({ + oldSomedayId: event._id, + newSomedayId: res.data._id, + }) + ); } else { - yield put(getWeekEventsSlice.actions.insert(res.data._id)); + yield put( + getWeekEventsSlice.actions.replace({ + oldWeekId: event._id, + newWeekId: res.data._id, + }) + ); } yield put( - eventsEntitiesSlice.actions.insert(normalizedEvent.entities.events) + eventsEntitiesSlice.actions.replace({ + oldEventId: event._id, + newEvent: res.data, + }) ); + yield put(createEventSlice.actions.success()); } catch (error) { yield put(createEventSlice.actions.error()); + yield call(deleteEvent, { + payload: { _id: event._id }, + } as Action_DeleteEvent); handleError(error as Error); } } export function* deleteEvent({ payload }: Action_DeleteEvent) { + // TODO: Ideally we should pass the entire event object instead of just its id + // to the `deleteEvent` payload. Gives us more context to work with. + const event = (yield select((state: RootState) => + selectEventById(state, payload._id) + )) as Schema_Event; + try { yield put(getWeekEventsSlice.actions.delete(payload)); yield put(eventsEntitiesSlice.actions.delete(payload)); - yield call(EventApi.delete, payload._id); + if (!event.optimistic) { + yield call(EventApi.delete, payload._id); + } + yield put(deleteEventSlice.actions.success()); } catch (error) { yield put(deleteEventSlice.actions.error()); diff --git a/packages/web/src/ducks/events/slices/event.slice.ts b/packages/web/src/ducks/events/slices/event.slice.ts index 03a59b92..0edbeb41 100644 --- a/packages/web/src/ducks/events/slices/event.slice.ts +++ b/packages/web/src/ducks/events/slices/event.slice.ts @@ -10,6 +10,7 @@ import { Action_DeleteEvent, Action_EditEvent, Action_InsertEvents, + Action_ReplaceEvent, Action_TimezoneChange, Entities_Event, Payload_EditEvent, @@ -52,6 +53,10 @@ export const eventsEntitiesSlice = createSlice({ insert: (state, action: Action_InsertEvents) => { state.value = { ...state.value, ...action.payload }; }, + replace: (state, action: Action_ReplaceEvent) => { + delete state.value[action.payload.oldEventId]; + state.value[action.payload.newEvent._id] = action.payload.newEvent; + }, updateAfterTzChange: (state, action: Action_TimezoneChange) => { const nextState = changeTimezones(state, action.payload.timezone); state.value = nextState.value; diff --git a/packages/web/src/ducks/events/slices/someday.slice.ts b/packages/web/src/ducks/events/slices/someday.slice.ts index 6558adff..d02cb1fe 100644 --- a/packages/web/src/ducks/events/slices/someday.slice.ts +++ b/packages/web/src/ducks/events/slices/someday.slice.ts @@ -29,6 +29,18 @@ export const getSomedayEventsSlice = createAsyncSlice< } }, + replace: ( + state, + action: { payload: { oldSomedayId: string; newSomedayId: string } } + ) => { + state.value.data = state.value.data.map((id: string) => { + if (id === action.payload.oldSomedayId) { + return action.payload.newSomedayId; + } + return id; + }); + }, + reorder: (state, action) => { return; }, diff --git a/packages/web/src/ducks/events/slices/week.slice.ts b/packages/web/src/ducks/events/slices/week.slice.ts index fea87112..09965487 100644 --- a/packages/web/src/ducks/events/slices/week.slice.ts +++ b/packages/web/src/ducks/events/slices/week.slice.ts @@ -25,5 +25,16 @@ export const getWeekEventsSlice = createAsyncSlice< state.value.data.push(action.payload); } }, + replace: ( + state, + action: { payload: { oldWeekId: string; newWeekId: string } } + ) => { + state.value.data = state.value.data.map((id: string) => { + if (id === action.payload.oldWeekId) { + return action.payload.newWeekId; + } + return id; + }); + }, }, });