From 23ca48db9a11c6dcc59e8c74be461b85d18b8f26 Mon Sep 17 00:00:00 2001 From: Muhammed Aldulaimi Date: Tue, 31 Dec 2024 07:48:33 +0300 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20Optimistically=20rend?= =?UTF-8?q?er=20when=20drag-and-dropping=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes [issue](https://github.com/SwitchbackTech/compass/issues/203) In this commit, whenever we drag and drop a someday event to the grid, we now handle: - Optimistically removing the someday event from the sidebar - Optimistically inserting the grid event We also error handle the optimistic flow and revert if needed --- .../web/src/common/types/web.event.types.ts | 4 ++ packages/web/src/common/utils/event.util.ts | 7 +++- .../web/src/ducks/events/sagas/event.sagas.ts | 38 ++++++++----------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/web/src/common/types/web.event.types.ts b/packages/web/src/common/types/web.event.types.ts index 17cdd1cf..9de12cff 100644 --- a/packages/web/src/common/types/web.event.types.ts +++ b/packages/web/src/common/types/web.event.types.ts @@ -15,6 +15,10 @@ export interface Schema_GridEvent extends Schema_Event { siblingsCount?: number; } +export interface Schema_OptimisticEvent extends Schema_Event { + _id: string; // We guarantee that we have an _id for optimistic events, unlike `Schema_Event` +} + export interface Schema_SelectedDates { startDate: Date; startTime: SelectOption; diff --git a/packages/web/src/common/utils/event.util.ts b/packages/web/src/common/utils/event.util.ts index 8b78d73e..0d8905a2 100644 --- a/packages/web/src/common/utils/event.util.ts +++ b/packages/web/src/common/utils/event.util.ts @@ -11,6 +11,7 @@ import { Status } from "@core/errors/status.codes"; import { Schema_GridEvent, + Schema_OptimisticEvent, Schema_SomedayEventsColumn, } from "../types/web.event.types"; import { removeGridFields } from "./grid.util"; @@ -211,8 +212,10 @@ export const prepEvtBeforeSubmit = (draft: Schema_GridEvent) => { return event; }; -export const createOptimisticEvent = (event: Schema_Event) => { - const _event: Schema_Event = { +export const createOptimisticEvent = ( + event: Schema_Event +): Schema_OptimisticEvent => { + const _event: Schema_OptimisticEvent = { ...event, _id: `${ID_OPTIMISTIC_PREFIX}-${uuidv4()}`, }; diff --git a/packages/web/src/ducks/events/sagas/event.sagas.ts b/packages/web/src/ducks/events/sagas/event.sagas.ts index 9dbb8330..65ceab80 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.ts @@ -46,6 +46,8 @@ import { } from "./event.saga.util"; function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { + let event = {}; + try { const { _id, updatedFields } = payload; @@ -53,35 +55,25 @@ function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { selectEventById(state, _id) )) as Response_GetEventsSaga; + // Optimistically convert the event + yield put(getSomedayEventsSlice.actions.remove({ _id })); + event = createOptimisticEvent({ + ...currEvent, + ...updatedFields, + }); + const optimisticId = event._id; + yield* insertOptimisticEvent(event, false); + const updatedEvent = { ...currEvent, ...updatedFields }; delete updatedEvent.order; delete updatedEvent.recurrence; const res = yield call(EventApi.edit, _id, updatedEvent); - const event = res.data as Schema_Event; - yield put(getWeekEventsSlice.actions.insert(event._id)); - - const normalizedEvent = normalize( - event, - normalizedEventsSchema() - ); - yield put( - eventsEntitiesSlice.actions.insert(normalizedEvent.entities.events) - ); - - const somedayEvents: Response_GetEventsSaga = (yield select( - (state: RootState) => selectPaginatedEventsBySectionType(state, "someday") - )) as Response_GetEventsSaga; - - const remainingSomedayEvents = somedayEvents.data.filter( - (id) => id !== _id - ); - yield put( - getSomedayEventsSlice.actions.success({ - data: remainingSomedayEvents, - }) - ); + const convertedEvent = res.data as Schema_Event; + yield* replaceOptimisticId(optimisticId, convertedEvent._id, false); } catch (error) { + yield put(eventsEntitiesSlice.actions.delete({ _id: event._id })); + yield put(getSomedayEventsSlice.actions.insert(payload._id)); yield put(getSomedayEventsSlice.actions.error()); handleError(error as Error); } From ddebf4830556e2ecb04d2591e27cd64ce1904288 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Tue, 31 Dec 2024 10:54:44 -0600 Subject: [PATCH 2/7] fix: omit someday properties from optimistic event 'order' and 'recurrence' aren't currently needed for a grid event --- .../web/src/ducks/events/sagas/event.sagas.ts | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/web/src/ducks/events/sagas/event.sagas.ts b/packages/web/src/ducks/events/sagas/event.sagas.ts index 65ceab80..4199b387 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.ts @@ -46,34 +46,38 @@ import { } from "./event.saga.util"; function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { - let event = {}; + const { _id, updatedFields } = payload; + let optimisticId: string | null = null; try { - const { _id, updatedFields } = payload; - const currEvent = (yield select((state: RootState) => selectEventById(state, _id) )) as Response_GetEventsSaga; + const gridEvent = { ...currEvent, ...updatedFields }; + delete gridEvent.order; + delete gridEvent.recurrence; + + const optimisticGridEvent = createOptimisticEvent(gridEvent); + optimisticId = optimisticGridEvent._id; + // Optimistically convert the event yield put(getSomedayEventsSlice.actions.remove({ _id })); - event = createOptimisticEvent({ - ...currEvent, - ...updatedFields, - }); - const optimisticId = event._id; - yield* insertOptimisticEvent(event, false); + yield* insertOptimisticEvent(optimisticGridEvent, false); - const updatedEvent = { ...currEvent, ...updatedFields }; - delete updatedEvent.order; - delete updatedEvent.recurrence; - - const res = yield call(EventApi.edit, _id, updatedEvent); + const res = yield call(EventApi.edit, _id, gridEvent); const convertedEvent = res.data as Schema_Event; - yield* replaceOptimisticId(optimisticId, convertedEvent._id, false); + + yield* replaceOptimisticId( + optimisticId, + convertedEvent._id as string, + false + ); } catch (error) { - yield put(eventsEntitiesSlice.actions.delete({ _id: event._id })); - yield put(getSomedayEventsSlice.actions.insert(payload._id)); + if (optimisticId) { + yield put(eventsEntitiesSlice.actions.delete({ _id: optimisticId })); + } + yield put(getSomedayEventsSlice.actions.insert(_id)); yield put(getSomedayEventsSlice.actions.error()); handleError(error as Error); } From c3541a33e0674bddee2503310690a40a974d3a47 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Tue, 31 Dec 2024 11:10:00 -0600 Subject: [PATCH 3/7] refactor (web): add createOptimisticGridEvent function to cleanup saga splitting things out of the convertSomedayEvent saga will make the saga more readable --- packages/web/src/common/utils/event.util.ts | 19 ++++++++++++++++--- packages/web/src/common/utils/grid.util.ts | 11 ++++++++--- .../web/src/ducks/events/sagas/event.sagas.ts | 12 ++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/web/src/common/utils/event.util.ts b/packages/web/src/common/utils/event.util.ts index 0d8905a2..39ab217f 100644 --- a/packages/web/src/common/utils/event.util.ts +++ b/packages/web/src/common/utils/event.util.ts @@ -8,13 +8,14 @@ import { YEAR_MONTH_DAY_COMPACT_FORMAT } from "@core/constants/date.constants"; import { Categories_Event, Schema_Event } from "@core/types/event.types"; import { Origin, Priorities } from "@core/constants/core.constants"; import { Status } from "@core/errors/status.codes"; +import { Response_GetEventsSaga } from "@web/ducks/events/event.types"; import { Schema_GridEvent, Schema_OptimisticEvent, Schema_SomedayEventsColumn, } from "../types/web.event.types"; -import { removeGridFields } from "./grid.util"; +import { removeGridProperties } from "./grid.util"; import { COLUMN_WEEK, COLUMN_MONTH, @@ -192,7 +193,7 @@ export const prepEvtAfterDraftDrop = ( }; export const prepEvtBeforeConvertToSomeday = (draft: Schema_GridEvent) => { - const event = removeGridFields(draft); + const event = removeGridProperties(draft); if (event.recurrence) { delete event.recurrence; @@ -202,7 +203,7 @@ export const prepEvtBeforeConvertToSomeday = (draft: Schema_GridEvent) => { }; export const prepEvtBeforeSubmit = (draft: Schema_GridEvent) => { - const _event = removeGridFields({ ...draft }); + const _event = removeGridProperties({ ...draft }); const event = { ..._event, @@ -222,3 +223,15 @@ export const createOptimisticEvent = ( return _event; }; + +export const createOptimisticGridEvent = ( + currentEvent: Response_GetEventsSaga, + updatedFields: Schema_GridEvent +) => { + const gridEvent = { ...currentEvent, ...updatedFields }; + delete gridEvent.order; + delete gridEvent.recurrence; + + const optimisticGridEvent = createOptimisticEvent(gridEvent); + return optimisticGridEvent; +}; diff --git a/packages/web/src/common/utils/grid.util.ts b/packages/web/src/common/utils/grid.util.ts index 1c7c633c..29718680 100644 --- a/packages/web/src/common/utils/grid.util.ts +++ b/packages/web/src/common/utils/grid.util.ts @@ -488,17 +488,22 @@ const normalizeDayNums = (days: number[]) => { }); }; -export const removeGridFields = (event: Schema_GridEvent): Schema_Event => { +export const removeGridProperties = (event: Schema_GridEvent): Schema_Event => { const { isEditing, importanceIndex, isOpen, row, siblingsCount, - ...eventWithoutGridFields + ...eventWithoutGridProps } = event; - return eventWithoutGridFields; + return eventWithoutGridProps; +}; + +export const removeSomedayProperties = (event: Schema_Event): Schema_Event => { + const { order, recurrence, ...eventWithoutSomedayProps } = event; + return eventWithoutSomedayProps; }; export const widthMinusPadding = (width: number) => { diff --git a/packages/web/src/ducks/events/sagas/event.sagas.ts b/packages/web/src/ducks/events/sagas/event.sagas.ts index 4199b387..18d23b59 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.ts @@ -11,6 +11,7 @@ import { selectEventById } from "@web/ducks/events/selectors/event.selectors"; import { selectPaginatedEventsBySectionType } from "@web/ducks/events/selectors/util.selectors"; import { createOptimisticEvent, + createOptimisticGridEvent, handleError, } from "@web/common/utils/event.util"; import { Schema_GridEvent } from "@web/common/types/web.event.types"; @@ -54,14 +55,13 @@ function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { selectEventById(state, _id) )) as Response_GetEventsSaga; - const gridEvent = { ...currEvent, ...updatedFields }; - delete gridEvent.order; - delete gridEvent.recurrence; - - const optimisticGridEvent = createOptimisticEvent(gridEvent); + // Optimistically convert the event + const optimisticGridEvent = createOptimisticGridEvent( + currEvent, + updatedFields + ); optimisticId = optimisticGridEvent._id; - // Optimistically convert the event yield put(getSomedayEventsSlice.actions.remove({ _id })); yield* insertOptimisticEvent(optimisticGridEvent, false); From 6893b7f6c5d36a4518e658b43982370e802b5598 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Wed, 1 Jan 2025 09:11:03 -0600 Subject: [PATCH 4/7] refactor: simplify optimistic event creation --- packages/web/src/common/utils/event.util.ts | 14 +------- packages/web/src/ducks/events/event.api.ts | 3 +- .../web/src/ducks/events/sagas/event.sagas.ts | 36 +++++++++++-------- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/web/src/common/utils/event.util.ts b/packages/web/src/common/utils/event.util.ts index 39ab217f..76017fef 100644 --- a/packages/web/src/common/utils/event.util.ts +++ b/packages/web/src/common/utils/event.util.ts @@ -213,7 +213,7 @@ export const prepEvtBeforeSubmit = (draft: Schema_GridEvent) => { return event; }; -export const createOptimisticEvent = ( +export const replaceIdWithOptimisticId = ( event: Schema_Event ): Schema_OptimisticEvent => { const _event: Schema_OptimisticEvent = { @@ -223,15 +223,3 @@ export const createOptimisticEvent = ( return _event; }; - -export const createOptimisticGridEvent = ( - currentEvent: Response_GetEventsSaga, - updatedFields: Schema_GridEvent -) => { - const gridEvent = { ...currentEvent, ...updatedFields }; - delete gridEvent.order; - delete gridEvent.recurrence; - - const optimisticGridEvent = createOptimisticEvent(gridEvent); - return optimisticGridEvent; -}; diff --git a/packages/web/src/ducks/events/event.api.ts b/packages/web/src/ducks/events/event.api.ts index c6a8da6a..6861d90b 100644 --- a/packages/web/src/ducks/events/event.api.ts +++ b/packages/web/src/ducks/events/event.api.ts @@ -5,6 +5,7 @@ import { Schema_Event, } from "@core/types/event.types"; import { CompassApi } from "@web/common/apis/compass.api"; +import { AxiosPromise } from "axios"; const EventApi = { create: (event: Schema_Event) => { @@ -17,7 +18,7 @@ const EventApi = { _id: string, event: Schema_Event, params: { applyTo?: Categories_Recur } - ) => { + ): AxiosPromise => { if (params?.applyTo) { return CompassApi.put(`/event/${_id}?applyTo=${params.applyTo}`, event); } diff --git a/packages/web/src/ducks/events/sagas/event.sagas.ts b/packages/web/src/ducks/events/sagas/event.sagas.ts index 18d23b59..136e5480 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.ts @@ -10,12 +10,12 @@ 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, - createOptimisticGridEvent, + replaceIdWithOptimisticId, handleError, } from "@web/common/utils/event.util"; import { Schema_GridEvent } from "@web/common/types/web.event.types"; import { ID_OPTIMISTIC_PREFIX } from "@web/common/constants/web.constants"; +import { AxiosResponse } from "axios"; import { createEventSlice, @@ -48,28 +48,36 @@ import { function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { const { _id, updatedFields } = payload; - let optimisticId: string | null = null; + const optimisticId: string | null = null; try { + //get grid event from store const currEvent = (yield select((state: RootState) => selectEventById(state, _id) )) as Response_GetEventsSaga; + const gridEvent = { ...currEvent, ...updatedFields }; + // remove extra props before sending to DB + delete gridEvent.order; + delete gridEvent.recurrence; - // Optimistically convert the event - const optimisticGridEvent = createOptimisticGridEvent( - currEvent, - updatedFields - ); - optimisticId = optimisticGridEvent._id; - + //get optimisitcGridEvent + const optimisticGridEvent = replaceIdWithOptimisticId(gridEvent); yield put(getSomedayEventsSlice.actions.remove({ _id })); yield* insertOptimisticEvent(optimisticGridEvent, false); - const res = yield call(EventApi.edit, _id, gridEvent); - const convertedEvent = res.data as Schema_Event; + // call API + const response = (yield call( + EventApi.edit, + _id, + gridEvent, + {} + )) as AxiosResponse; + + const convertedEvent = response.data; + // replace ids yield* replaceOptimisticId( - optimisticId, + optimisticGridEvent._id, convertedEvent._id as string, false ); @@ -115,7 +123,7 @@ function* convertTimedEvent({ payload }: Action_ConvertTimedEvent) { } function* createEvent({ payload }: Action_CreateEvent) { - const event = createOptimisticEvent(payload); + const event = replaceIdWithOptimisticId(payload); const optimisticId = event._id; try { From 98ba5d91c7befd2f92399b085413ade44b6c9850 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 2 Jan 2025 07:18:28 -0600 Subject: [PATCH 5/7] refactor(web): separate someday and regular event sagas this is to make it easier to understand sagas work --- .../web/src/ducks/events/sagas/event.sagas.ts | 124 +----------------- .../{event.saga.util.ts => saga.util.ts} | 0 .../src/ducks/events/sagas/somday.sagas.ts | 118 +++++++++++++++++ packages/web/src/store/sagas.ts | 46 ++++++- 4 files changed, 167 insertions(+), 121 deletions(-) rename packages/web/src/ducks/events/sagas/{event.saga.util.ts => saga.util.ts} (100%) create mode 100644 packages/web/src/ducks/events/sagas/somday.sagas.ts diff --git a/packages/web/src/ducks/events/sagas/event.sagas.ts b/packages/web/src/ducks/events/sagas/event.sagas.ts index 136e5480..751f4d0f 100644 --- a/packages/web/src/ducks/events/sagas/event.sagas.ts +++ b/packages/web/src/ducks/events/sagas/event.sagas.ts @@ -1,6 +1,6 @@ import { normalize } from "normalizr"; import dayjs from "dayjs"; -import { call, put, takeLatest, select } from "@redux-saga/core/effects"; +import { call, put, select } from "@redux-saga/core/effects"; import { Params_Events, Schema_Event } from "@core/types/event.types"; import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants"; import { RootState } from "@web/store"; @@ -15,7 +15,6 @@ import { } from "@web/common/utils/event.util"; import { Schema_GridEvent } from "@web/common/types/web.event.types"; import { ID_OPTIMISTIC_PREFIX } from "@web/common/constants/web.constants"; -import { AxiosResponse } from "axios"; import { createEventSlice, @@ -26,7 +25,6 @@ import { } from "../slices/event.slice"; import { getWeekEventsSlice } from "../slices/week.slice"; import { - Action_ConvertSomedayEvent, Action_ConvertTimedEvent, Action_CreateEvent, Action_DeleteEvent, @@ -39,59 +37,13 @@ import { Entities_Event, } from "../event.types"; import { getSomedayEventsSlice } from "../slices/someday.slice"; -import { Action_Someday_Reorder } from "../slices/someday.slice.types"; import { insertOptimisticEvent, normalizedEventsSchema, replaceOptimisticId, -} from "./event.saga.util"; - -function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { - const { _id, updatedFields } = payload; - const optimisticId: string | null = null; +} from "./saga.util"; - try { - //get grid event from store - const currEvent = (yield select((state: RootState) => - selectEventById(state, _id) - )) as Response_GetEventsSaga; - const gridEvent = { ...currEvent, ...updatedFields }; - // remove extra props before sending to DB - delete gridEvent.order; - delete gridEvent.recurrence; - - //get optimisitcGridEvent - const optimisticGridEvent = replaceIdWithOptimisticId(gridEvent); - yield put(getSomedayEventsSlice.actions.remove({ _id })); - yield* insertOptimisticEvent(optimisticGridEvent, false); - - // call API - const response = (yield call( - EventApi.edit, - _id, - gridEvent, - {} - )) as AxiosResponse; - - const convertedEvent = response.data; - - // replace ids - yield* replaceOptimisticId( - optimisticGridEvent._id, - convertedEvent._id as string, - false - ); - } catch (error) { - if (optimisticId) { - yield put(eventsEntitiesSlice.actions.delete({ _id: optimisticId })); - } - yield put(getSomedayEventsSlice.actions.insert(_id)); - yield put(getSomedayEventsSlice.actions.error()); - handleError(error as Error); - } -} - -function* convertTimedEvent({ payload }: Action_ConvertTimedEvent) { +export function* convertTimedEvent({ payload }: Action_ConvertTimedEvent) { try { const res = yield call(EventApi.edit, payload.event._id, payload.event); const event = res.data as Schema_Event; @@ -122,7 +74,7 @@ function* convertTimedEvent({ payload }: Action_ConvertTimedEvent) { } } -function* createEvent({ payload }: Action_CreateEvent) { +export function* createEvent({ payload }: Action_CreateEvent) { const event = replaceIdWithOptimisticId(payload); const optimisticId = event._id; @@ -167,18 +119,6 @@ export function* deleteEvent({ payload }: Action_DeleteEvent) { } } -export function* deleteSomedayEvent({ payload }: Action_DeleteEvent) { - try { - yield put(eventsEntitiesSlice.actions.delete(payload)); - - yield call(EventApi.delete, payload._id); - } catch (error) { - yield put(getSomedayEventsSlice.actions.error()); - handleError(error as Error); - yield put(getSomedayEventsSlice.actions.request()); - } -} - export function* editEvent({ payload }: Action_EditEvent) { const { _id, applyTo, event, shouldRemove } = payload; @@ -197,7 +137,7 @@ export function* editEvent({ payload }: Action_EditEvent) { } } -function* getCurrentMonthEvents({ payload }: Action_GetPaginatedEvents) { +export function* getCurrentMonthEvents({ payload }: Action_GetPaginatedEvents) { try { const startDate = dayjs().startOf("month").format(YEAR_MONTH_DAY_FORMAT); const endDate = dayjs().endOf("month").format(YEAR_MONTH_DAY_FORMAT); @@ -242,31 +182,7 @@ function* getEvents( } } -export function* getSomedayEvents({ payload }: Action_GetEvents) { - try { - const res = (yield call(EventApi.get, { - someday: true, - startDate: payload.startDate, - endDate: payload.endDate, - })) as Response_GetEventsSuccess; - - const normalizedEvents = normalize(res.data, [ - normalizedEventsSchema(), - ]); - yield put( - eventsEntitiesSlice.actions.insert(normalizedEvents.entities.events) - ); - - const data = { - data: normalizedEvents.result as Payload_NormalizedAsyncAction, - }; - yield put(getSomedayEventsSlice.actions.success(data)); - } catch (error) { - yield put(getSomedayEventsSlice.actions.error()); - } -} - -function* getWeekEvents({ payload }: Action_GetEvents) { +export function* getWeekEvents({ payload }: Action_GetEvents) { try { const data = (yield call(getEvents, payload)) as Response_GetEventsSaga; yield put(getWeekEventsSlice.actions.success(data)); @@ -275,31 +191,3 @@ function* getWeekEvents({ payload }: Action_GetEvents) { handleError(error as Error); } } - -function* reorderSomedayEvents({ payload }: Action_Someday_Reorder) { - try { - yield call(EventApi.reorder, payload); - } catch (error) { - yield put(getSomedayEventsSlice.actions.error()); - handleError(error as Error); - } -} - -/************ - * Assemble - ***********/ -export function* eventsSagas() { - yield takeLatest(getWeekEventsSlice.actions.request, getWeekEvents); - yield takeLatest(getWeekEventsSlice.actions.convert, convertTimedEvent); - yield takeLatest( - getCurrentMonthEventsSlice.actions.request, - getCurrentMonthEvents - ); - yield takeLatest(getSomedayEventsSlice.actions.convert, convertSomedayEvent); - yield takeLatest(getSomedayEventsSlice.actions.request, getSomedayEvents); - yield takeLatest(getSomedayEventsSlice.actions.delete, deleteSomedayEvent); - yield takeLatest(getSomedayEventsSlice.actions.reorder, reorderSomedayEvents); - yield takeLatest(createEventSlice.actions.request, createEvent); - yield takeLatest(editEventSlice.actions.request, editEvent); - yield takeLatest(deleteEventSlice.actions.request, deleteEvent); -} diff --git a/packages/web/src/ducks/events/sagas/event.saga.util.ts b/packages/web/src/ducks/events/sagas/saga.util.ts similarity index 100% rename from packages/web/src/ducks/events/sagas/event.saga.util.ts rename to packages/web/src/ducks/events/sagas/saga.util.ts diff --git a/packages/web/src/ducks/events/sagas/somday.sagas.ts b/packages/web/src/ducks/events/sagas/somday.sagas.ts new file mode 100644 index 00000000..663997dc --- /dev/null +++ b/packages/web/src/ducks/events/sagas/somday.sagas.ts @@ -0,0 +1,118 @@ +import { AxiosResponse } from "axios"; +import { normalize } from "normalizr"; +import { call, put, select } from "redux-saga/effects"; +import { Schema_Event } from "@core/types/event.types"; +import { Payload_NormalizedAsyncAction } from "@web/common/types/entity.types"; +import { + replaceIdWithOptimisticId, + handleError, +} from "@web/common/utils/event.util"; +import { RootState } from "@web/store"; + +import { Action_Someday_Reorder } from "../slices/someday.slice.types"; +import { EventApi } from "../event.api"; +import { + Action_ConvertSomedayEvent, + Response_GetEventsSaga, + Action_DeleteEvent, + Action_GetEvents, + Response_GetEventsSuccess, +} from "../event.types"; +import { selectEventById } from "../selectors/event.selectors"; +import { eventsEntitiesSlice } from "../slices/event.slice"; +import { getSomedayEventsSlice } from "../slices/someday.slice"; +import { + insertOptimisticEvent, + replaceOptimisticId, + normalizedEventsSchema, +} from "./saga.util"; + +export function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { + const { _id, updatedFields } = payload; + const optimisticId: string | null = null; + + try { + //get grid event from store + const currEvent = (yield select((state: RootState) => + selectEventById(state, _id) + )) as Response_GetEventsSaga; + const gridEvent = { ...currEvent, ...updatedFields }; + // remove extra props before sending to DB + delete gridEvent.order; + delete gridEvent.recurrence; + + //get optimisitcGridEvent + const optimisticGridEvent = replaceIdWithOptimisticId(gridEvent); + yield put(getSomedayEventsSlice.actions.remove({ _id })); + yield* insertOptimisticEvent(optimisticGridEvent, false); + + // call API + const response = (yield call( + EventApi.edit, + _id, + gridEvent, + {} + )) as AxiosResponse; + + const convertedEvent = response.data; + + // replace ids + yield* replaceOptimisticId( + optimisticGridEvent._id, + convertedEvent._id as string, + false + ); + } catch (error) { + if (optimisticId) { + yield put(eventsEntitiesSlice.actions.delete({ _id: optimisticId })); + } + yield put(getSomedayEventsSlice.actions.insert(_id)); + yield put(getSomedayEventsSlice.actions.error()); + handleError(error as Error); + } +} + +export function* deleteSomedayEvent({ payload }: Action_DeleteEvent) { + try { + yield put(eventsEntitiesSlice.actions.delete(payload)); + + yield call(EventApi.delete, payload._id); + } catch (error) { + yield put(getSomedayEventsSlice.actions.error()); + handleError(error as Error); + yield put(getSomedayEventsSlice.actions.request()); + } +} + +export function* getSomedayEvents({ payload }: Action_GetEvents) { + try { + const res = (yield call(EventApi.get, { + someday: true, + startDate: payload.startDate, + endDate: payload.endDate, + })) as Response_GetEventsSuccess; + + const normalizedEvents = normalize(res.data, [ + normalizedEventsSchema(), + ]); + yield put( + eventsEntitiesSlice.actions.insert(normalizedEvents.entities.events) + ); + + const data = { + data: normalizedEvents.result as Payload_NormalizedAsyncAction, + }; + yield put(getSomedayEventsSlice.actions.success(data)); + } catch (error) { + yield put(getSomedayEventsSlice.actions.error()); + } +} + +export function* reorderSomedayEvents({ payload }: Action_Someday_Reorder) { + try { + yield call(EventApi.reorder, payload); + } catch (error) { + yield put(getSomedayEventsSlice.actions.error()); + handleError(error as Error); + } +} diff --git a/packages/web/src/store/sagas.ts b/packages/web/src/store/sagas.ts index ff0f0e09..dab07003 100644 --- a/packages/web/src/store/sagas.ts +++ b/packages/web/src/store/sagas.ts @@ -1,6 +1,46 @@ -import { all } from "@redux-saga/core/effects"; -import { eventsSagas } from "@web/ducks/events/sagas/event.sagas"; +import { all, takeLatest } from "redux-saga/effects"; +import { + getWeekEvents, + convertTimedEvent, + getCurrentMonthEvents, + editEvent, + deleteEvent, + createEvent, +} from "@web/ducks/events/sagas/event.sagas"; +import { + convertSomedayEvent, + getSomedayEvents, + deleteSomedayEvent, + reorderSomedayEvents, +} from "@web/ducks/events/sagas/somday.sagas"; +import { + getCurrentMonthEventsSlice, + createEventSlice, + editEventSlice, + deleteEventSlice, +} from "@web/ducks/events/slices/event.slice"; +import { getSomedayEventsSlice } from "@web/ducks/events/slices/someday.slice"; +import { getWeekEventsSlice } from "@web/ducks/events/slices/week.slice"; export function* sagas() { - yield all([eventsSagas()]); + yield all([eventSagas(), somedayEventSagas()]); +} + +function* eventSagas() { + yield takeLatest(getWeekEventsSlice.actions.request, getWeekEvents); + yield takeLatest(getWeekEventsSlice.actions.convert, convertTimedEvent); + yield takeLatest( + getCurrentMonthEventsSlice.actions.request, + getCurrentMonthEvents + ); + yield takeLatest(createEventSlice.actions.request, createEvent); + yield takeLatest(editEventSlice.actions.request, editEvent); + yield takeLatest(deleteEventSlice.actions.request, deleteEvent); +} + +function* somedayEventSagas() { + yield takeLatest(getSomedayEventsSlice.actions.convert, convertSomedayEvent); + yield takeLatest(getSomedayEventsSlice.actions.request, getSomedayEvents); + yield takeLatest(getSomedayEventsSlice.actions.delete, deleteSomedayEvent); + yield takeLatest(getSomedayEventsSlice.actions.reorder, reorderSomedayEvents); } From e9e03e81a6461b6455352429180192706dc1d963 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 2 Jan 2025 09:30:06 -0600 Subject: [PATCH 6/7] refactor(web): create utility saga to use during someday event conversion --- .../web/src/ducks/events/sagas/saga.util.ts | 11 +++++- .../src/ducks/events/sagas/somday.sagas.ts | 34 +++++++++++-------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/web/src/ducks/events/sagas/saga.util.ts b/packages/web/src/ducks/events/sagas/saga.util.ts index ecd760bf..ada06282 100644 --- a/packages/web/src/ducks/events/sagas/saga.util.ts +++ b/packages/web/src/ducks/events/sagas/saga.util.ts @@ -1,12 +1,21 @@ import { schema } from "normalizr"; -import { put } from "redux-saga/effects"; +import { put, select } from "redux-saga/effects"; import { normalize } from "normalizr"; import { Schema_Event } from "@core/types/event.types"; import { Schema_GridEvent } from "@web/common/types/web.event.types"; +import { RootState } from "@web/store"; import { getSomedayEventsSlice } from "../slices/someday.slice"; import { getWeekEventsSlice } from "../slices/week.slice"; import { eventsEntitiesSlice } from "../slices/event.slice"; +import { selectEventById } from "../selectors/event.selectors"; + +export function* getEventById(_id: string) { + const currEvent = (yield select((state: RootState) => + selectEventById(state, _id) + )) as Schema_Event; + return currEvent; +} export function* insertOptimisticEvent( event: Schema_GridEvent, diff --git a/packages/web/src/ducks/events/sagas/somday.sagas.ts b/packages/web/src/ducks/events/sagas/somday.sagas.ts index 663997dc..f9086391 100644 --- a/packages/web/src/ducks/events/sagas/somday.sagas.ts +++ b/packages/web/src/ducks/events/sagas/somday.sagas.ts @@ -1,30 +1,30 @@ import { AxiosResponse } from "axios"; import { normalize } from "normalizr"; -import { call, put, select } from "redux-saga/effects"; +import { call, put } from "redux-saga/effects"; import { Schema_Event } from "@core/types/event.types"; import { Payload_NormalizedAsyncAction } from "@web/common/types/entity.types"; import { replaceIdWithOptimisticId, handleError, } from "@web/common/utils/event.util"; -import { RootState } from "@web/store"; +import { Schema_GridEvent } from "@web/common/types/web.event.types"; +import { removeSomedayProperties } from "@web/common/utils/grid.util"; import { Action_Someday_Reorder } from "../slices/someday.slice.types"; import { EventApi } from "../event.api"; import { Action_ConvertSomedayEvent, - Response_GetEventsSaga, Action_DeleteEvent, Action_GetEvents, Response_GetEventsSuccess, } from "../event.types"; -import { selectEventById } from "../selectors/event.selectors"; import { eventsEntitiesSlice } from "../slices/event.slice"; import { getSomedayEventsSlice } from "../slices/someday.slice"; import { insertOptimisticEvent, replaceOptimisticId, normalizedEventsSchema, + getEventById, } from "./saga.util"; export function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { @@ -32,21 +32,14 @@ export function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { const optimisticId: string | null = null; try { - //get grid event from store - const currEvent = (yield select((state: RootState) => - selectEventById(state, _id) - )) as Response_GetEventsSaga; - const gridEvent = { ...currEvent, ...updatedFields }; - // remove extra props before sending to DB - delete gridEvent.order; - delete gridEvent.recurrence; + const gridEvent = yield* _assembleGridEvent(_id, updatedFields); - //get optimisitcGridEvent + //create optimisitcGridEvent const optimisticGridEvent = replaceIdWithOptimisticId(gridEvent); yield put(getSomedayEventsSlice.actions.remove({ _id })); yield* insertOptimisticEvent(optimisticGridEvent, false); - // call API + // create real event const response = (yield call( EventApi.edit, _id, @@ -56,7 +49,7 @@ export function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { const convertedEvent = response.data; - // replace ids + // cleanup replace ids yield* replaceOptimisticId( optimisticGridEvent._id, convertedEvent._id as string, @@ -116,3 +109,14 @@ export function* reorderSomedayEvents({ payload }: Action_Someday_Reorder) { handleError(error as Error); } } + +function* _assembleGridEvent( + _id: string, + updatedFields: Partial +) { + const currEvent = yield* getEventById(_id); + + const _gridEvent = { ...currEvent, ...updatedFields }; + const gridEvent = removeSomedayProperties(_gridEvent); + return gridEvent as Schema_GridEvent; +} From 543cc01b94b3cec17ace679cc0f4446f6e8355ea Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 3 Jan 2025 06:22:54 -0600 Subject: [PATCH 7/7] refactor(web): cleanup convertSomedayEvent saga by separating logic into smaller generators --- .../src/ducks/events/sagas/somday.sagas.ts | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/web/src/ducks/events/sagas/somday.sagas.ts b/packages/web/src/ducks/events/sagas/somday.sagas.ts index f9086391..5e62f1ab 100644 --- a/packages/web/src/ducks/events/sagas/somday.sagas.ts +++ b/packages/web/src/ducks/events/sagas/somday.sagas.ts @@ -32,29 +32,13 @@ export function* convertSomedayEvent({ payload }: Action_ConvertSomedayEvent) { const optimisticId: string | null = null; try { + yield put(getSomedayEventsSlice.actions.remove({ _id })); + const gridEvent = yield* _assembleGridEvent(_id, updatedFields); - //create optimisitcGridEvent - const optimisticGridEvent = replaceIdWithOptimisticId(gridEvent); - yield put(getSomedayEventsSlice.actions.remove({ _id })); - yield* insertOptimisticEvent(optimisticGridEvent, false); - - // create real event - const response = (yield call( - EventApi.edit, - _id, - gridEvent, - {} - )) as AxiosResponse; - - const convertedEvent = response.data; - - // cleanup replace ids - yield* replaceOptimisticId( - optimisticGridEvent._id, - convertedEvent._id as string, - false - ); + const optimisticId = yield* _createOptimisticGridEvent(gridEvent); + const persistentId = yield* _convertEvent(gridEvent); + yield* replaceOptimisticId(optimisticId, persistentId as string, false); } catch (error) { if (optimisticId) { yield put(eventsEntitiesSlice.actions.delete({ _id: optimisticId })); @@ -120,3 +104,23 @@ function* _assembleGridEvent( const gridEvent = removeSomedayProperties(_gridEvent); return gridEvent as Schema_GridEvent; } + +function* _convertEvent(gridEvent: Schema_GridEvent) { + const response = (yield call( + EventApi.edit, + gridEvent._id as string, + gridEvent, + {} + )) as AxiosResponse; + + const convertedEvent = response.data; + return convertedEvent._id; +} + +function* _createOptimisticGridEvent(gridEvent: Schema_GridEvent) { + const optimisticGridEvent = replaceIdWithOptimisticId(gridEvent); + + yield* insertOptimisticEvent(optimisticGridEvent, false); + + return optimisticGridEvent._id; +}