From 5a5a6511ca14eb760dd8111b453ff20a853e988e Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 22 Dec 2023 12:37:36 +0530 Subject: [PATCH] chore: revert back to optimistic update approach for all `update related actions` (#3219) --- web/store/cycle.store.ts | 68 +++++++++++++++---- web/store/label/project-label.store.ts | 19 ++++-- web/store/member/project-member.store.ts | 26 +++++-- web/store/member/workspace-member.store.ts | 25 +++++-- web/store/module.store.ts | 79 +++++++++++++++------- web/store/state.store.ts | 54 ++++++++++----- web/store/user/index.ts | 74 +++++++++++++------- 7 files changed, 249 insertions(+), 96 deletions(-) diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index 2f7320b878..3e54907140 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -88,6 +88,9 @@ export class CycleStore implements ICycleStore { } // computed + /** + * returns all cycle ids for a project + */ get projectCycleIds() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; @@ -97,6 +100,9 @@ export class CycleStore implements ICycleStore { return allCycles || null; } + /** + * returns all completed cycle ids for a project + */ get projectCompletedCycleIds() { const allCycles = this.projectCycleIds; if (!allCycles) return null; @@ -107,6 +113,9 @@ export class CycleStore implements ICycleStore { return completedCycles || null; } + /** + * returns all upcoming cycle ids for a project + */ get projectUpcomingCycleIds() { const allCycles = this.projectCycleIds; if (!allCycles) return null; @@ -117,6 +126,9 @@ export class CycleStore implements ICycleStore { return upcomingCycles || null; } + /** + * returns all incomplete cycle ids for a project + */ get projectIncompleteCycleIds() { const allCycles = this.projectCycleIds; if (!allCycles) return null; @@ -127,6 +139,9 @@ export class CycleStore implements ICycleStore { return incompleteCycles || null; } + /** + * returns all draft cycle ids for a project + */ get projectDraftCycleIds() { const allCycles = this.projectCycleIds; if (!allCycles) return null; @@ -137,6 +152,9 @@ export class CycleStore implements ICycleStore { return draftCycles || null; } + /** + * returns active cycle id for a project + */ get projectActiveCycleId() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; @@ -242,14 +260,21 @@ export class CycleStore implements ICycleStore { * @param data * @returns */ - updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial) => - await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data).then((response) => { + updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial) => { + try { runInAction(() => { set(this.cycleMap, [cycleId], { ...this.cycleMap?.[cycleId], ...data }); set(this.activeCycleMap, [cycleId], { ...this.activeCycleMap?.[cycleId], ...data }); }); + const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); return response; - }); + } catch (error) { + console.log("Failed to patch cycle from cycle store"); + this.fetchAllCycles(workspaceSlug, projectId); + this.fetchActiveCycle(workspaceSlug, projectId); + throw error; + } + }; /** * @description deletes a cycle @@ -272,16 +297,25 @@ export class CycleStore implements ICycleStore { * @param cycleId * @returns */ - addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => - await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId }).then((response) => { - const currentCycle = this.getCycleById(cycleId); - const currentActiveCycle = this.getActiveCycleById(cycleId); + addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); + try { runInAction(() => { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true); if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true); }); + // updating through api. + const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId }); return response; - }); + } catch (error) { + runInAction(() => { + if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false); + if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false); + }); + throw error; + } + }; /** * @description removes a cycle from favorites @@ -290,14 +324,22 @@ export class CycleStore implements ICycleStore { * @param cycleId * @returns */ - removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => - await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId).then((response) => { - const currentCycle = this.getCycleById(cycleId); - const currentActiveCycle = this.getActiveCycleById(cycleId); + removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); + try { runInAction(() => { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false); if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false); }); + const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); return response; - }); + } catch (error) { + runInAction(() => { + if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true); + if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true); + }); + throw error; + } + }; } diff --git a/web/store/label/project-label.store.ts b/web/store/label/project-label.store.ts index d2767db2f8..79a7932bfe 100644 --- a/web/store/label/project-label.store.ts +++ b/web/store/label/project-label.store.ts @@ -120,11 +120,20 @@ export class ProjectLabelStore implements IProjectLabelStore { * @returns Promise */ updateLabel = async (workspaceSlug: string, projectId: string, labelId: string, data: Partial) => { - runInAction(() => { - set(this.labelMap, [labelId], { ...this.labelMap?.[labelId], ...data }); - }); - const response = await this.issueLabelService.patchIssueLabel(workspaceSlug, projectId, labelId, data); - return response; + const originalLabel = this.labelMap[labelId]; + try { + runInAction(() => { + set(this.labelMap, [labelId], { ...this.labelMap[labelId], ...data }); + }); + const response = await this.issueLabelService.patchIssueLabel(workspaceSlug, projectId, labelId, data); + return response; + } catch (error) { + console.log("Failed to update label from project store"); + runInAction(() => { + set(this.labelMap, [labelId], originalLabel); + }); + throw error; + } }; /** diff --git a/web/store/member/project-member.store.ts b/web/store/member/project-member.store.ts index 2f6b415741..4cd4077ff8 100644 --- a/web/store/member/project-member.store.ts +++ b/web/store/member/project-member.store.ts @@ -150,14 +150,26 @@ export class ProjectMemberStore implements IProjectMemberStore { ) => { const memberDetails = this.getProjectMemberDetails(userId); if (!memberDetails) throw new Error("Member not found"); - return await this.projectMemberService - .updateProjectMember(workspaceSlug, projectId, memberDetails?.id, data) - .then((response) => { - runInAction(() => { - set(this.projectMemberMap, [projectId, userId, "role"], data.role); - }); - return response; + // original data to revert back in case of error + const originalProjectMemberData = this.projectMemberMap?.[projectId]?.[userId]; + try { + runInAction(() => { + set(this.projectMemberMap, [projectId, userId, "role"], data.role); + }); + const response = await this.projectMemberService.updateProjectMember( + workspaceSlug, + projectId, + memberDetails?.id, + data + ); + return response; + } catch (error) { + // revert back to original members in case of error + runInAction(() => { + set(this.projectMemberMap, [projectId, userId], originalProjectMemberData); }); + throw error; + } }; /** diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts index 64ed8465ce..8085a3edc0 100644 --- a/web/store/member/workspace-member.store.ts +++ b/web/store/member/workspace-member.store.ts @@ -199,11 +199,20 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { updateMember = async (workspaceSlug: string, userId: string, data: { role: EUserWorkspaceRoles }) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) throw new Error("Member not found"); - await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberDetails.id, data).then(() => { + // original data to revert back in case of error + const originalProjectMemberData = this.workspaceMemberMap?.[workspaceSlug]?.[userId]; + try { runInAction(() => { set(this.workspaceMemberMap, [workspaceSlug, userId, "role"], data.role); }); - }); + await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberDetails.id, data); + } catch (error) { + // revert back to original members in case of error + runInAction(() => { + set(this.workspaceMemberMap, [workspaceSlug, userId], originalProjectMemberData); + }); + throw error; + } }; /** @@ -257,15 +266,23 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { data: Partial ) => { const originalMemberInvitations = [...this.workspaceMemberInvitations?.[workspaceSlug]]; // in case of error, we will revert back to original members - await this.workspaceService.updateWorkspaceInvitation(workspaceSlug, invitationId, data).then(() => { + try { const memberInvitations = originalMemberInvitations?.map((invitation) => ({ ...invitation, ...(invitation.id === invitationId && data), })); + // optimistic update runInAction(() => { set(this.workspaceMemberInvitations, workspaceSlug, memberInvitations); }); - }); + await this.workspaceService.updateWorkspaceInvitation(workspaceSlug, invitationId, data); + } catch (error) { + // revert back to original members in case of error + runInAction(() => { + set(this.workspaceMemberInvitations, workspaceSlug, originalMemberInvitations); + }); + throw error; + } }; /** diff --git a/web/store/module.store.ts b/web/store/module.store.ts index a80e0dffdc..2d87cfbdef 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -84,6 +84,9 @@ export class ModulesStore implements IModuleStore { } // computed + /** + * get all module ids for the current project + */ get projectModuleIds() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; @@ -155,14 +158,22 @@ export class ModulesStore implements IModuleStore { * @param data * @returns IModule */ - updateModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => - await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data).then((response) => { - const moduleDetails = this.getModuleById(moduleId); + updateModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => { + const originalModuleDetails = this.getModuleById(moduleId); + try { runInAction(() => { - set(this.moduleMap, [moduleId], { ...moduleDetails, ...data }); + set(this.moduleMap, [moduleId], { ...originalModuleDetails, ...data }); }); + const response = await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data); return response; - }); + } catch (error) { + console.error("Failed to update module in module store", error); + runInAction(() => { + set(this.moduleMap, [moduleId], { ...originalModuleDetails }); + }); + throw error; + } + }; /** * @description deletes a module @@ -211,15 +222,25 @@ export class ModulesStore implements IModuleStore { moduleId: string, linkId: string, data: Partial - ) => - await this.moduleService.updateModuleLink(workspaceSlug, projectId, moduleId, linkId, data).then((response) => { - const moduleDetails = this.getModuleById(moduleId); - const linkModules = moduleDetails?.link_module.map((link) => (link.id === linkId ? response : link)); + ) => { + const originalModuleDetails = this.getModuleById(moduleId); + try { + const linkModules = originalModuleDetails?.link_module.map((link) => + link.id === linkId ? { ...link, ...data } : link + ); runInAction(() => { set(this.moduleMap, [moduleId, "link_module"], linkModules); }); + const response = await this.moduleService.updateModuleLink(workspaceSlug, projectId, moduleId, linkId, data); return response; - }); + } catch (error) { + console.error("Failed to update module link in module store", error); + runInAction(() => { + set(this.moduleMap, [moduleId, "link_module"], originalModuleDetails?.link_module); + }); + throw error; + } + }; /** * @description deletes a module link @@ -244,18 +265,23 @@ export class ModulesStore implements IModuleStore { * @param moduleId * @returns */ - addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => - await this.moduleService - .addModuleToFavorites(workspaceSlug, projectId, { + addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + const moduleDetails = this.getModuleById(moduleId); + if (moduleDetails?.is_favorite) return; + runInAction(() => { + set(this.moduleMap, [moduleId, "is_favorite"], true); + }); + await this.moduleService.addModuleToFavorites(workspaceSlug, projectId, { module: moduleId, - }) - .then(() => { - const moduleDetails = this.getModuleById(moduleId); - if (moduleDetails?.is_favorite) return; - runInAction(() => { - set(this.moduleMap, [moduleId, "is_favorite"], true); - }); }); + } catch (error) { + console.error("Failed to add module to favorites in module store", error); + runInAction(() => { + set(this.moduleMap, [moduleId, "is_favorite"], false); + }); + } + }; /** * @description removes a module from favorites @@ -264,12 +290,19 @@ export class ModulesStore implements IModuleStore { * @param moduleId * @returns */ - removeModuleFromFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => - await this.moduleService.removeModuleFromFavorites(workspaceSlug, projectId, moduleId).then(() => { + removeModuleFromFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { const moduleDetails = this.getModuleById(moduleId); if (!moduleDetails?.is_favorite) return; runInAction(() => { set(this.moduleMap, [moduleId, "is_favorite"], false); }); - }); + await this.moduleService.removeModuleFromFavorites(workspaceSlug, projectId, moduleId); + } catch (error) { + console.error("Failed to remove module from favorites in module store", error); + runInAction(() => { + set(this.moduleMap, [moduleId, "is_favorite"], true); + }); + } + }; } diff --git a/web/store/state.store.ts b/web/store/state.store.ts index 9f34959611..ad1d063f2f 100644 --- a/web/store/state.store.ts +++ b/web/store/state.store.ts @@ -129,13 +129,24 @@ export class StateStore implements IStateStore { * @param data * @returns */ - updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => - await this.stateService.patchState(workspaceSlug, projectId, stateId, data).then((response) => { + updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => { + const originalState = this.stateMap[stateId]; + try { runInAction(() => { set(this.stateMap, [stateId], { ...this.stateMap?.[stateId], ...data }); }); + const response = await this.stateService.patchState(workspaceSlug, projectId, stateId, data); return response; - }); + } catch (error) { + runInAction(() => { + this.stateMap = { + ...this.stateMap, + [stateId]: originalState, + }; + }); + throw error; + } + }; /** * deletes the state from the store, incase of failure reverts back to original state @@ -194,23 +205,30 @@ export class StateStore implements IStateStore { groupIndex: number ) => { const SEQUENCE_GAP = 15000; - let newSequence = SEQUENCE_GAP; - const stateMap = this.projectStates || []; - const selectedState = stateMap?.find((state) => state.id === stateId); - const groupStates = stateMap?.filter((state) => state.group === selectedState?.group); - const groupLength = groupStates.length; - if (direction === "up") { - if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; - else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; - } else { - if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; - else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; - } - // updating using api - await this.stateService.patchState(workspaceSlug, projectId, stateId, { sequence: newSequence }).then(() => { + const originalStates = this.stateMap; + try { + let newSequence = SEQUENCE_GAP; + const stateMap = this.projectStates || []; + const selectedState = stateMap?.find((state) => state.id === stateId); + const groupStates = stateMap?.filter((state) => state.group === selectedState?.group); + const groupLength = groupStates.length; + if (direction === "up") { + if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; + } else { + if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; + } runInAction(() => { set(this.stateMap, [stateId, "sequence"], newSequence); }); - }); + // updating using api + await this.stateService.patchState(workspaceSlug, projectId, stateId, { sequence: newSequence }); + } catch (err) { + // reverting back to old state group if api fails + runInAction(() => { + this.stateMap = originalStates; + }); + } }; } diff --git a/web/store/user/index.ts b/web/store/user/index.ts index db74765bb3..e7a4988bb4 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -151,16 +151,20 @@ export class UserRootStore implements IUserRootStore { * @returns Promise */ updateUserOnBoard = async () => { - const user = this.currentUser ?? undefined; - if (!user) return; - await this.userService.updateUserOnBoard().then(() => { + try { runInAction(() => { this.currentUser = { ...this.currentUser, is_onboarded: true, } as IUser; }); - }); + const user = this.currentUser ?? undefined; + if (!user) return; + await this.userService.updateUserOnBoard(); + } catch (error) { + this.fetchCurrentUser(); + throw error; + } }; /** @@ -168,15 +172,20 @@ export class UserRootStore implements IUserRootStore { * @returns Promise */ updateTourCompleted = async () => { - if (this.currentUser) { - return await this.userService.updateUserTourCompleted().then(() => { + try { + if (this.currentUser) { runInAction(() => { this.currentUser = { ...this.currentUser, is_tour_completed: true, } as IUser; }); - }); + const response = await this.userService.updateUserTourCompleted(); + return response; + } + } catch (error) { + this.fetchCurrentUser(); + throw error; } }; @@ -185,36 +194,49 @@ export class UserRootStore implements IUserRootStore { * @param data * @returns Promise */ - updateCurrentUser = async (data: Partial) => - await this.userService.updateUser(data).then((response) => { + updateCurrentUser = async (data: Partial) => { + try { + runInAction(() => { + this.currentUser = { + ...this.currentUser, + ...data, + } as IUser; + }); + const response = await this.userService.updateUser(data); runInAction(() => { this.currentUser = response; }); return response; - }); + } catch (error) { + this.fetchCurrentUser(); + throw error; + } + }; /** * Updates the current user theme * @param theme * @returns Promise */ - updateCurrentUserTheme = async (theme: string) => - await this.userService - .updateUser({ - theme: { ...this.currentUser?.theme, theme }, - } as IUser) - .then((response) => { - runInAction(() => { - this.currentUser = { - ...this.currentUser, - theme: { - ...this.currentUser?.theme, - theme, - }, - } as IUser; - }); - return response; + updateCurrentUserTheme = async (theme: string) => { + try { + runInAction(() => { + this.currentUser = { + ...this.currentUser, + theme: { + ...this.currentUser?.theme, + theme, + }, + } as IUser; }); + const response = await this.userService.updateUser({ + theme: { ...this.currentUser?.theme, theme }, + } as IUser); + return response; + } catch (error) { + throw error; + } + }; /** * Deactivates the current user