From 780b239ecb8782d6e37f37797b2373a1a94e74db Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 7 May 2024 14:05:56 +0530 Subject: [PATCH] [WEB-1142] chore: optimistically add issue to cycle/modules (#4334) * chore: optimistically add issue to cycle and module * chore: update toast alerts * refactor: module issue store * chore: added addCycleToIssueFunction --- .../issues/issue-detail/cycle-select.tsx | 2 +- web/components/issues/issue-detail/root.tsx | 83 ++++++++------ web/components/issues/peek-overview/root.tsx | 101 +++++++++++------- web/hooks/use-issues-actions.tsx | 13 ++- web/services/issue/issue.service.ts | 10 +- web/services/module.service.ts | 17 +-- web/store/issue/cycle/issue.store.ts | 47 +++++++- web/store/issue/issue-details/issue.store.ts | 20 +++- web/store/issue/issue-details/root.store.ts | 2 + web/store/issue/issue.store.ts | 6 +- web/store/issue/module/issue.store.ts | 73 +++++++------ web/store/issue/root.store.ts | 2 +- 12 files changed, 235 insertions(+), 141 deletions(-) diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index 77e854768..0d4fc8889 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -34,7 +34,7 @@ export const IssueCycleSelect: React.FC = observer((props) => const handleIssueCycleChange = async (cycleId: string | null) => { if (!issue || issue.cycle_id === cycleId) return; setIsUpdating(true); - if (cycleId) await issueOperations.addIssueToCycle?.(workspaceSlug, projectId, cycleId, [issueId]); + if (cycleId) await issueOperations.addCycleToIssue?.(workspaceSlug, projectId, cycleId, issueId); else await issueOperations.removeIssueFromCycle?.(workspaceSlug, projectId, issue.cycle_id ?? "", issueId); setIsUpdating(false); }; diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index c061c6a63..037a7b13d 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -26,6 +26,7 @@ export type TIssueOperations = { remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + addCycleToIssue?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -62,6 +63,7 @@ export const IssueDetailRoot: FC = observer((props) => { updateIssue, removeIssue, archiveIssue, + addCycleToIssue, addIssueToCycle, removeIssueFromCycle, addModulesToIssue, @@ -158,21 +160,38 @@ export const IssueDetailRoot: FC = observer((props) => { }); } }, + addCycleToIssue: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + try { + await addCycleToIssue(workspaceSlug, projectId, cycleId, issueId); + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { issueId, state: "SUCCESS", element: "Issue detail page" }, + updates: { + changed_property: "cycle_id", + change_details: cycleId, + }, + path: router.asPath, + }); + } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Issue could not be added to the cycle. Please try again.", + }); + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { state: "FAILED", element: "Issue detail page" }, + updates: { + changed_property: "cycle_id", + change_details: cycleId, + }, + path: router.asPath, + }); + } + }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { - const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); - setPromiseToast(addToCyclePromise, { - loading: "Adding cycle to issue...", - success: { - title: "Success!", - message: () => "Cycle added to issue successfully", - }, - error: { - title: "Error!", - message: () => "Cycle add to issue failed", - }, - }); - await addToCyclePromise; + await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" }, @@ -183,6 +202,11 @@ export const IssueDetailRoot: FC = observer((props) => { path: router.asPath, }); } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Issue could not be added to the cycle. Please try again.", + }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue detail page" }, @@ -198,14 +222,14 @@ export const IssueDetailRoot: FC = observer((props) => { try { const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); setPromiseToast(removeFromCyclePromise, { - loading: "Removing cycle from issue...", + loading: "Removing issue from the cycle...", success: { title: "Success!", - message: () => "Cycle removed from issue successfully", + message: () => "Issue removed from the cycle successfully.", }, error: { title: "Error!", - message: () => "Cycle remove from issue failed", + message: () => "Issue could not be removed from the cycle. Please try again.", }, }); await removeFromCyclePromise; @@ -232,19 +256,7 @@ export const IssueDetailRoot: FC = observer((props) => { }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { - const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); - setPromiseToast(addToModulePromise, { - loading: "Adding module to issue...", - success: { - title: "Success!", - message: () => "Module added to issue successfully", - }, - error: { - title: "Error!", - message: () => "Module add to issue failed", - }, - }); - const response = await addToModulePromise; + const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, @@ -255,6 +267,11 @@ export const IssueDetailRoot: FC = observer((props) => { path: router.asPath, }); } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Issue could not be added to the module. Please try again.", + }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, @@ -270,14 +287,14 @@ export const IssueDetailRoot: FC = observer((props) => { try { const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); setPromiseToast(removeFromModulePromise, { - loading: "Removing module from issue...", + loading: "Removing issue from the module...", success: { title: "Success!", - message: () => "Module removed from issue successfully", + message: () => "Issue removed from the module successfully.", }, error: { title: "Error!", - message: () => "Module remove from issue failed", + message: () => "Issue could not be removed from the module. Please try again.", }, }); await removeFromModulePromise; @@ -335,6 +352,8 @@ export const IssueDetailRoot: FC = observer((props) => { addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, + captureIssueEvent, + router.asPath, ] ); diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 2ddc923a0..eb5b46652 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -1,19 +1,18 @@ import { FC, useEffect, useState, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; +// types import { TIssue } from "@plane/types"; -// hooks -import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; -import { IssueView } from "@/components/issues"; // ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components +import { IssueView } from "@/components/issues"; +// constants import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "@/constants/event-tracker"; import { EIssuesStoreType } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; +// hooks import { useEventTracker, useIssueDetail, useIssues, useUser } from "@/hooks/store"; -// components -// types -// constants interface IIssuePeekOverview { is_archived?: boolean; @@ -60,8 +59,14 @@ export const IssuePeekOverview: FC = observer((props) => { archiveIssue, issue: { getIssueById, fetchIssue }, } = useIssueDetail(); - const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } = - useIssueDetail(); + const { + addCycleToIssue, + addIssueToCycle, + removeIssueFromCycle, + addModulesToIssue, + removeIssueFromModule, + removeModulesFromIssue, + } = useIssueDetail(); const { captureIssueEvent } = useEventTracker(); // state const [loader, setLoader] = useState(false); @@ -174,21 +179,39 @@ export const IssuePeekOverview: FC = observer((props) => { }); } }, + addCycleToIssue: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + try { + console.log("Peek adding..."); + await addCycleToIssue(workspaceSlug, projectId, cycleId, issueId); + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" }, + updates: { + changed_property: "cycle_id", + change_details: cycleId, + }, + path: router.asPath, + }); + } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Issue could not be added to the cycle. Please try again.", + }); + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { state: "FAILED", element: "Issue peek-overview" }, + updates: { + changed_property: "cycle_id", + change_details: cycleId, + }, + path: router.asPath, + }); + } + }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { - const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); - setPromiseToast(addToCyclePromise, { - loading: "Adding cycle to issue...", - success: { - title: "Success!", - message: () => "Cycle added to issue successfully", - }, - error: { - title: "Error!", - message: () => "Cycle add to issue failed", - }, - }); - await addToCyclePromise; + await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" }, @@ -199,6 +222,11 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Issue could not be added to the cycle. Please try again.", + }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue peek-overview" }, @@ -214,14 +242,14 @@ export const IssuePeekOverview: FC = observer((props) => { try { const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); setPromiseToast(removeFromCyclePromise, { - loading: "Removing cycle from issue...", + loading: "Removing issue from the cycle...", success: { title: "Success!", - message: () => "Cycle removed from issue successfully", + message: () => "Issue removed from the cycle successfully.", }, error: { title: "Error!", - message: () => "Cycle remove from issue failed", + message: () => "Issue could not be removed from the cycle. Please try again.", }, }); await removeFromCyclePromise; @@ -248,19 +276,7 @@ export const IssuePeekOverview: FC = observer((props) => { }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { - const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); - setPromiseToast(addToModulePromise, { - loading: "Adding module to issue...", - success: { - title: "Success!", - message: () => "Module added to issue successfully", - }, - error: { - title: "Error!", - message: () => "Module add to issue failed", - }, - }); - const response = await addToModulePromise; + const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, @@ -271,6 +287,11 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Issue could not be added to the module. Please try again.", + }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, @@ -286,14 +307,14 @@ export const IssuePeekOverview: FC = observer((props) => { try { const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); setPromiseToast(removeFromModulePromise, { - loading: "Removing module from issue...", + loading: "Removing issue from the module...", success: { title: "Success!", - message: () => "Module removed from issue successfully", + message: () => "Issue removed from the module successfully.", }, error: { title: "Error!", - message: () => "Module remove from issue failed", + message: () => "Issue could not be removed from the module. Please try again.", }, }); await removeFromModulePromise; diff --git a/web/hooks/use-issues-actions.tsx b/web/hooks/use-issues-actions.tsx index f77513f21..30f293de7 100644 --- a/web/hooks/use-issues-actions.tsx +++ b/web/hooks/use-issues-actions.tsx @@ -1,5 +1,5 @@ -import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; -import { useApplication, useIssues } from "./store"; +import { useCallback, useMemo } from "react"; +// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, @@ -8,7 +8,10 @@ import { TIssueKanbanFilters, TLoader, } from "@plane/types"; -import { useCallback, useMemo } from "react"; +// constants +import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; +// hooks +import { useApplication, useIssues } from "./store"; interface IssueActions { fetchIssues?: (projectId: string, loadType: TLoader) => Promise; @@ -238,9 +241,9 @@ const useModuleIssueActions = () => { const removeIssueFromView = useCallback( async (projectId: string, issueId: string) => { if (!moduleId || !workspaceSlug) return; - return await issues.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + return await issues.removeIssuesFromModule(workspaceSlug, projectId, moduleId, [issueId]); }, - [issues.removeIssueFromModule, moduleId, workspaceSlug] + [issues.removeIssuesFromModule, moduleId, workspaceSlug] ); const archiveIssue = useCallback( async (projectId: string, issueId: string) => { diff --git a/web/services/issue/issue.service.ts b/web/services/issue/issue.service.ts index d4a195d76..1f6d95fd6 100644 --- a/web/services/issue/issue.service.ts +++ b/web/services/issue/issue.service.ts @@ -1,9 +1,9 @@ -// services -import { API_BASE_URL } from "@/helpers/common.helper"; -import { APIService } from "@/services/api.service"; -// type +// types import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity } from "@plane/types"; -// helper +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; export class IssueService extends APIService { constructor() { diff --git a/web/services/module.service.ts b/web/services/module.service.ts index 10c74fc8b..ec330cf95 100644 --- a/web/services/module.service.ts +++ b/web/services/module.service.ts @@ -1,8 +1,8 @@ +// types +import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types"; // services import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; -// types -import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types"; export class ModuleService extends APIService { constructor() { @@ -106,19 +106,6 @@ export class ModuleService extends APIService { }); } - async removeIssueFromModule( - workspaceSlug: string, - projectId: string, - moduleId: string, - issueId: string - ): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async removeIssuesFromModuleBulk( workspaceSlug: string, projectId: string, diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 45f7bb05d..6524311a1 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -4,12 +4,12 @@ import set from "lodash/set"; import uniq from "lodash/uniq"; import update from "lodash/update"; import { action, observable, makeObservable, computed, runInAction } from "mobx"; -// base class +// types +import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; // services import { CycleService } from "@/services/cycle.service"; import { IssueService } from "@/services/issue"; // types -import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; import { IIssueRootStore } from "../root.store"; @@ -59,6 +59,7 @@ export interface ICycleIssues { fetchAddedIssues?: boolean ) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; + addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; transferIssuesFromCycle: ( workspaceSlug: string, projectId: string, @@ -101,6 +102,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { quickAddIssue: action, addIssueToCycle: action, removeIssueFromCycle: action, + addCycleToIssue: action, transferIssuesFromCycle: action, fetchActiveCycleIssues: action, }); @@ -303,18 +305,22 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); + // add the new issue ids to the cycle issues map runInAction(() => { update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds))); }); issueIds.forEach((issueId) => { const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id; + // remove issue from previous cycle if it exists if (issueCycleId && issueCycleId !== cycleId) { runInAction(() => { pull(this.issues[issueCycleId], issueId); }); } + // update the root issue map with the new cycle id this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId }); }); + this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); } catch (error) { throw error; @@ -336,6 +342,43 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { } }; + addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id; + try { + // add the new issue ids to the cycle issues map + runInAction(() => { + update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, [issueId]))); + }); + // remove issue from previous cycle if it exists + if (issueCycleId && issueCycleId !== cycleId) { + runInAction(() => { + pull(this.issues[issueCycleId], issueId); + }); + } + // update the root issue map with the new cycle id + this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId }); + + await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, { + issues: [issueId], + }); + + this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); + } catch (error) { + // remove the new issue ids from the cycle issues map + runInAction(() => { + pull(this.issues[cycleId], issueId); + }); + // add issue back to the previous cycle if it exists + if (issueCycleId) + runInAction(() => { + update(this.issues, issueCycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, [issueId]))); + }); + // update the root issue map with the original cycle id + this.rootStore.issues.updateIssue(issueId, { cycle_id: issueCycleId ?? null }); + throw error; + } + }; + transferIssuesFromCycle = async ( workspaceSlug: string, projectId: string, diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index fe28557c7..370a34cd2 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -1,9 +1,10 @@ import { makeObservable } from "mobx"; -// services import { computedFn } from "mobx-utils"; -import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue"; // types import { TIssue } from "@plane/types"; +// services +import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue"; +// types import { IIssueDetail } from "./root.store"; export interface IIssueStoreActions { @@ -17,6 +18,7 @@ export interface IIssueStoreActions { updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -161,6 +163,16 @@ export class IssueStore implements IIssueStore { archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => this.rootIssueDetailStore.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); + addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addCycleToIssue( + workspaceSlug, + projectId, + cycleId, + issueId + ); + await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + }; + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle( workspaceSlug, @@ -208,11 +220,11 @@ export class IssueStore implements IIssueStore { }; removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { - const currentModule = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeIssueFromModule( + const currentModule = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeIssuesFromModule( workspaceSlug, projectId, moduleId, - issueId + [issueId] ); await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return currentModule; diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index c2d890806..90695a934 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -191,6 +191,8 @@ export class IssueDetail implements IIssueDetail { this.issue.removeIssue(workspaceSlug, projectId, issueId); archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => this.issue.archiveIssue(workspaceSlug, projectId, issueId); + addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => + this.issue.addCycleToIssue(workspaceSlug, projectId, cycleId, issueId); addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index b2b33464c..e6418c11d 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -1,13 +1,13 @@ import isEmpty from "lodash/isEmpty"; import set from "lodash/set"; -// store import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types -import { IssueService } from "@/services/issue"; import { TIssue } from "@plane/types"; +// helpers import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; -//services +// services +import { IssueService } from "@/services/issue"; export type IIssueStore = { // observables diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index 73c68bdde..02433bcba 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -4,13 +4,14 @@ import set from "lodash/set"; import uniq from "lodash/uniq"; import update from "lodash/update"; import { action, observable, makeObservable, computed, runInAction } from "mobx"; -// base class +// types +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; // services import { IssueService } from "@/services/issue"; import { ModuleService } from "@/services/module.service"; -// types -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +// helpers import { IssueHelperStore } from "../helpers/issue-helper.store"; +// store import { IIssueRootStore } from "../root.store"; export interface IModuleIssues { @@ -69,7 +70,6 @@ export interface IModuleIssues { issueId: string, moduleIds: string[] ) => Promise; - removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise; } export class ModuleIssues extends IssueHelperStore implements IModuleIssues { @@ -106,7 +106,6 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { removeIssuesFromModule: action, addModulesToIssue: action, removeModulesFromIssue: action, - removeIssueFromModule: action, }); this.rootIssueStore = _rootStore; @@ -301,27 +300,39 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { fetchAddedIssues = true ) => { try { - await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, { - issues: issueIds, - }); - - if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); - + // add the new issue ids to the module issues map runInAction(() => { update(this.issues, moduleId, (moduleIssueIds = []) => { if (!moduleIssueIds) return [...issueIds]; else return uniq(concat(moduleIssueIds, issueIds)); }); }); - + // update the root issue map with the new module ids issueIds.forEach((issueId) => { update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => { if (issueModuleIds.includes(moduleId)) return issueModuleIds; else return uniq(concat(issueModuleIds, [moduleId])); }); }); + + await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, { + issues: issueIds, + }); + + if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); + this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); } catch (error) { + issueIds.forEach((issueId) => { + runInAction(() => { + // remove the new issue ids from the module issues map + pull(this.issues[moduleId], issueId); + // remove the new module ids from the root issue map + update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => + pull(issueModuleIds, moduleId) + ); + }); + }); throw error; } }; @@ -358,25 +369,38 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { }; addModulesToIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { + // keep a copy of the original module ids + const originalModuleIds = this.rootStore.issues.issuesMap[issueId]?.module_ids ?? []; try { - const issueToModule = await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, { - modules: moduleIds, - }); - runInAction(() => { + // add the new issue ids to the module issues map moduleIds.forEach((moduleId) => { update(this.issues, moduleId, (moduleIssueIds = []) => { if (moduleIssueIds.includes(issueId)) return moduleIssueIds; else return uniq(concat(moduleIssueIds, [issueId])); }); }); + // update the root issue map with the new module ids update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => uniq(concat(issueModuleIds, moduleIds)) ); }); + const issueToModule = await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, { + modules: moduleIds, + }); + return issueToModule; } catch (error) { + // revert the issue back to its original module ids + set(this.rootStore.issues.issuesMap, [issueId, "module_ids"], originalModuleIds); + // remove the new issue ids from the module issues map + moduleIds.forEach((moduleId) => { + runInAction(() => { + update(this.issues, moduleId, (moduleIssueIds = []) => pull(moduleIssueIds, issueId)); + }); + }); + throw error; } }; @@ -407,21 +431,4 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { throw error; } }; - - removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { - try { - runInAction(() => { - pull(this.issues[moduleId], issueId); - update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => - pull(issueModuleIds, moduleId) - ); - }); - - const response = await this.moduleService.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); - - return response; - } catch (error) { - throw error; - } - }; } diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts index dc7c94f53..e1f1f8d03 100644 --- a/web/store/issue/root.store.ts +++ b/web/store/issue/root.store.ts @@ -1,8 +1,8 @@ import isEmpty from "lodash/isEmpty"; import { autorun, makeObservable, observable } from "mobx"; +import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; // root store import { IWorkspaceMembership } from "@/store/member/workspace-member.store"; -import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; import { RootStore } from "../root.store"; import { IStateStore, StateStore } from "../state.store"; // issues data store