[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
This commit is contained in:
Aaryan Khandelwal 2024-05-07 14:05:56 +05:30 committed by GitHub
parent a85517de99
commit 780b239ecb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 235 additions and 141 deletions

View File

@ -34,7 +34,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
const handleIssueCycleChange = async (cycleId: string | null) => { const handleIssueCycleChange = async (cycleId: string | null) => {
if (!issue || issue.cycle_id === cycleId) return; if (!issue || issue.cycle_id === cycleId) return;
setIsUpdating(true); 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); else await issueOperations.removeIssueFromCycle?.(workspaceSlug, projectId, issue.cycle_id ?? "", issueId);
setIsUpdating(false); setIsUpdating(false);
}; };

View File

@ -26,6 +26,7 @@ export type TIssueOperations = {
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addCycleToIssue?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>; addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>; removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>; addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
@ -62,6 +63,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
updateIssue, updateIssue,
removeIssue, removeIssue,
archiveIssue, archiveIssue,
addCycleToIssue,
addIssueToCycle, addIssueToCycle,
removeIssueFromCycle, removeIssueFromCycle,
addModulesToIssue, addModulesToIssue,
@ -158,21 +160,38 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = 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[]) => { addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
try { try {
const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); await 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;
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" }, payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" },
@ -183,6 +202,11 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
path: router.asPath, path: router.asPath,
}); });
} catch (error) { } catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be added to the cycle. Please try again.",
});
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue detail page" }, payload: { state: "FAILED", element: "Issue detail page" },
@ -198,14 +222,14 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
try { try {
const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
setPromiseToast(removeFromCyclePromise, { setPromiseToast(removeFromCyclePromise, {
loading: "Removing cycle from issue...", loading: "Removing issue from the cycle...",
success: { success: {
title: "Success!", title: "Success!",
message: () => "Cycle removed from issue successfully", message: () => "Issue removed from the cycle successfully.",
}, },
error: { error: {
title: "Error!", title: "Error!",
message: () => "Cycle remove from issue failed", message: () => "Issue could not be removed from the cycle. Please try again.",
}, },
}); });
await removeFromCyclePromise; await removeFromCyclePromise;
@ -232,19 +256,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
}, },
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
try { try {
const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); const response = await 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;
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
@ -255,6 +267,11 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
path: router.asPath, path: router.asPath,
}); });
} catch (error) { } catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be added to the module. Please try again.",
});
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
@ -270,14 +287,14 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
try { try {
const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
setPromiseToast(removeFromModulePromise, { setPromiseToast(removeFromModulePromise, {
loading: "Removing module from issue...", loading: "Removing issue from the module...",
success: { success: {
title: "Success!", title: "Success!",
message: () => "Module removed from issue successfully", message: () => "Issue removed from the module successfully.",
}, },
error: { error: {
title: "Error!", title: "Error!",
message: () => "Module remove from issue failed", message: () => "Issue could not be removed from the module. Please try again.",
}, },
}); });
await removeFromModulePromise; await removeFromModulePromise;
@ -335,6 +352,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
addModulesToIssue, addModulesToIssue,
removeIssueFromModule, removeIssueFromModule,
removeModulesFromIssue, removeModulesFromIssue,
captureIssueEvent,
router.asPath,
] ]
); );

View File

@ -1,19 +1,18 @@
import { FC, useEffect, useState, useMemo } from "react"; import { FC, useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// hooks
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
import { IssueView } from "@/components/issues";
// ui // ui
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// components // components
import { IssueView } from "@/components/issues";
// constants
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "@/constants/event-tracker"; import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "@/constants/event-tracker";
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project"; import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useEventTracker, useIssueDetail, useIssues, useUser } from "@/hooks/store"; import { useEventTracker, useIssueDetail, useIssues, useUser } from "@/hooks/store";
// components
// types
// constants
interface IIssuePeekOverview { interface IIssuePeekOverview {
is_archived?: boolean; is_archived?: boolean;
@ -60,8 +59,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
archiveIssue, archiveIssue,
issue: { getIssueById, fetchIssue }, issue: { getIssueById, fetchIssue },
} = useIssueDetail(); } = useIssueDetail();
const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } = const {
useIssueDetail(); addCycleToIssue,
addIssueToCycle,
removeIssueFromCycle,
addModulesToIssue,
removeIssueFromModule,
removeModulesFromIssue,
} = useIssueDetail();
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
// state // state
const [loader, setLoader] = useState(false); const [loader, setLoader] = useState(false);
@ -174,21 +179,39 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = 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[]) => { addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
try { try {
const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); await 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;
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" }, payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" },
@ -199,6 +222,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
path: router.asPath, path: router.asPath,
}); });
} catch (error) { } catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be added to the cycle. Please try again.",
});
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { state: "FAILED", element: "Issue peek-overview" }, payload: { state: "FAILED", element: "Issue peek-overview" },
@ -214,14 +242,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
try { try {
const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
setPromiseToast(removeFromCyclePromise, { setPromiseToast(removeFromCyclePromise, {
loading: "Removing cycle from issue...", loading: "Removing issue from the cycle...",
success: { success: {
title: "Success!", title: "Success!",
message: () => "Cycle removed from issue successfully", message: () => "Issue removed from the cycle successfully.",
}, },
error: { error: {
title: "Error!", title: "Error!",
message: () => "Cycle remove from issue failed", message: () => "Issue could not be removed from the cycle. Please try again.",
}, },
}); });
await removeFromCyclePromise; await removeFromCyclePromise;
@ -248,19 +276,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}, },
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
try { try {
const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); const response = await 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;
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
@ -271,6 +287,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
path: router.asPath, path: router.asPath,
}); });
} catch (error) { } catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Issue could not be added to the module. Please try again.",
});
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
@ -286,14 +307,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
try { try {
const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
setPromiseToast(removeFromModulePromise, { setPromiseToast(removeFromModulePromise, {
loading: "Removing module from issue...", loading: "Removing issue from the module...",
success: { success: {
title: "Success!", title: "Success!",
message: () => "Module removed from issue successfully", message: () => "Issue removed from the module successfully.",
}, },
error: { error: {
title: "Error!", title: "Error!",
message: () => "Module remove from issue failed", message: () => "Issue could not be removed from the module. Please try again.",
}, },
}); });
await removeFromModulePromise; await removeFromModulePromise;

View File

@ -1,5 +1,5 @@
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { useCallback, useMemo } from "react";
import { useApplication, useIssues } from "./store"; // types
import { import {
IIssueDisplayFilterOptions, IIssueDisplayFilterOptions,
IIssueDisplayProperties, IIssueDisplayProperties,
@ -8,7 +8,10 @@ import {
TIssueKanbanFilters, TIssueKanbanFilters,
TLoader, TLoader,
} from "@plane/types"; } from "@plane/types";
import { useCallback, useMemo } from "react"; // constants
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
// hooks
import { useApplication, useIssues } from "./store";
interface IssueActions { interface IssueActions {
fetchIssues?: (projectId: string, loadType: TLoader) => Promise<TIssue[] | undefined>; fetchIssues?: (projectId: string, loadType: TLoader) => Promise<TIssue[] | undefined>;
@ -238,9 +241,9 @@ const useModuleIssueActions = () => {
const removeIssueFromView = useCallback( const removeIssueFromView = useCallback(
async (projectId: string, issueId: string) => { async (projectId: string, issueId: string) => {
if (!moduleId || !workspaceSlug) return; 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( const archiveIssue = useCallback(
async (projectId: string, issueId: string) => { async (projectId: string, issueId: string) => {

View File

@ -1,9 +1,9 @@
// services // types
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
// type
import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity } from "@plane/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 { export class IssueService extends APIService {
constructor() { constructor() {

View File

@ -1,8 +1,8 @@
// types
import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types";
// services // services
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service"; import { APIService } from "@/services/api.service";
// types
import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types";
export class ModuleService extends APIService { export class ModuleService extends APIService {
constructor() { constructor() {
@ -106,19 +106,6 @@ export class ModuleService extends APIService {
}); });
} }
async removeIssueFromModule(
workspaceSlug: string,
projectId: string,
moduleId: string,
issueId: string
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/issues/${issueId}/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async removeIssuesFromModuleBulk( async removeIssuesFromModuleBulk(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,

View File

@ -4,12 +4,12 @@ import set from "lodash/set";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import update from "lodash/update"; import update from "lodash/update";
import { action, observable, makeObservable, computed, runInAction } from "mobx"; import { action, observable, makeObservable, computed, runInAction } from "mobx";
// base class // types
import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types";
// services // services
import { CycleService } from "@/services/cycle.service"; import { CycleService } from "@/services/cycle.service";
import { IssueService } from "@/services/issue"; import { IssueService } from "@/services/issue";
// types // types
import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types";
import { IssueHelperStore } from "../helpers/issue-helper.store"; import { IssueHelperStore } from "../helpers/issue-helper.store";
import { IIssueRootStore } from "../root.store"; import { IIssueRootStore } from "../root.store";
@ -59,6 +59,7 @@ export interface ICycleIssues {
fetchAddedIssues?: boolean fetchAddedIssues?: boolean
) => Promise<void>; ) => Promise<void>;
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
transferIssuesFromCycle: ( transferIssuesFromCycle: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -101,6 +102,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
quickAddIssue: action, quickAddIssue: action,
addIssueToCycle: action, addIssueToCycle: action,
removeIssueFromCycle: action, removeIssueFromCycle: action,
addCycleToIssue: action,
transferIssuesFromCycle: action, transferIssuesFromCycle: action,
fetchActiveCycleIssues: action, fetchActiveCycleIssues: action,
}); });
@ -303,18 +305,22 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds);
// add the new issue ids to the cycle issues map
runInAction(() => { runInAction(() => {
update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds))); update(this.issues, cycleId, (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds)));
}); });
issueIds.forEach((issueId) => { issueIds.forEach((issueId) => {
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id; const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
// remove issue from previous cycle if it exists
if (issueCycleId && issueCycleId !== cycleId) { if (issueCycleId && issueCycleId !== cycleId) {
runInAction(() => { runInAction(() => {
pull(this.issues[issueCycleId], issueId); pull(this.issues[issueCycleId], issueId);
}); });
} }
// update the root issue map with the new cycle id
this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId }); this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId });
}); });
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
} catch (error) { } catch (error) {
throw 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 ( transferIssuesFromCycle = async (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,

View File

@ -1,9 +1,10 @@
import { makeObservable } from "mobx"; import { makeObservable } from "mobx";
// services
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// services
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
// types
import { IIssueDetail } from "./root.store"; import { IIssueDetail } from "./root.store";
export interface IIssueStoreActions { export interface IIssueStoreActions {
@ -17,6 +18,7 @@ export interface IIssueStoreActions {
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>; addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
@ -161,6 +163,16 @@ export class IssueStore implements IIssueStore {
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.rootIssueDetailStore.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); 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[]) => { addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle( await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(
workspaceSlug, workspaceSlug,
@ -208,11 +220,11 @@ export class IssueStore implements IIssueStore {
}; };
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { 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, workspaceSlug,
projectId, projectId,
moduleId, moduleId,
issueId [issueId]
); );
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
return currentModule; return currentModule;

View File

@ -191,6 +191,8 @@ export class IssueDetail implements IIssueDetail {
this.issue.removeIssue(workspaceSlug, projectId, issueId); this.issue.removeIssue(workspaceSlug, projectId, issueId);
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.issue.archiveIssue(workspaceSlug, projectId, issueId); 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[]) => addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) =>
this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>

View File

@ -1,13 +1,13 @@
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import set from "lodash/set"; import set from "lodash/set";
// store
import { action, makeObservable, observable, runInAction } from "mobx"; import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
// types // types
import { IssueService } from "@/services/issue";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// helpers
import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper";
// services // services
import { IssueService } from "@/services/issue";
export type IIssueStore = { export type IIssueStore = {
// observables // observables

View File

@ -4,13 +4,14 @@ import set from "lodash/set";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import update from "lodash/update"; import update from "lodash/update";
import { action, observable, makeObservable, computed, runInAction } from "mobx"; import { action, observable, makeObservable, computed, runInAction } from "mobx";
// base class // types
import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types";
// services // services
import { IssueService } from "@/services/issue"; import { IssueService } from "@/services/issue";
import { ModuleService } from "@/services/module.service"; import { ModuleService } from "@/services/module.service";
// types // helpers
import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types";
import { IssueHelperStore } from "../helpers/issue-helper.store"; import { IssueHelperStore } from "../helpers/issue-helper.store";
// store
import { IIssueRootStore } from "../root.store"; import { IIssueRootStore } from "../root.store";
export interface IModuleIssues { export interface IModuleIssues {
@ -69,7 +70,6 @@ export interface IModuleIssues {
issueId: string, issueId: string,
moduleIds: string[] moduleIds: string[]
) => Promise<void>; ) => Promise<void>;
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
} }
export class ModuleIssues extends IssueHelperStore implements IModuleIssues { export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
@ -106,7 +106,6 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
removeIssuesFromModule: action, removeIssuesFromModule: action,
addModulesToIssue: action, addModulesToIssue: action,
removeModulesFromIssue: action, removeModulesFromIssue: action,
removeIssueFromModule: action,
}); });
this.rootIssueStore = _rootStore; this.rootIssueStore = _rootStore;
@ -301,27 +300,39 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
fetchAddedIssues = true fetchAddedIssues = true
) => { ) => {
try { try {
await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, { // add the new issue ids to the module issues map
issues: issueIds,
});
if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds);
runInAction(() => { runInAction(() => {
update(this.issues, moduleId, (moduleIssueIds = []) => { update(this.issues, moduleId, (moduleIssueIds = []) => {
if (!moduleIssueIds) return [...issueIds]; if (!moduleIssueIds) return [...issueIds];
else return uniq(concat(moduleIssueIds, issueIds)); else return uniq(concat(moduleIssueIds, issueIds));
}); });
}); });
// update the root issue map with the new module ids
issueIds.forEach((issueId) => { issueIds.forEach((issueId) => {
update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => { update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => {
if (issueModuleIds.includes(moduleId)) return issueModuleIds; if (issueModuleIds.includes(moduleId)) return issueModuleIds;
else return uniq(concat(issueModuleIds, [moduleId])); 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); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
} catch (error) { } 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; throw error;
} }
}; };
@ -358,25 +369,38 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
}; };
addModulesToIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { 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 { try {
const issueToModule = await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
modules: moduleIds,
});
runInAction(() => { runInAction(() => {
// add the new issue ids to the module issues map
moduleIds.forEach((moduleId) => { moduleIds.forEach((moduleId) => {
update(this.issues, moduleId, (moduleIssueIds = []) => { update(this.issues, moduleId, (moduleIssueIds = []) => {
if (moduleIssueIds.includes(issueId)) return moduleIssueIds; if (moduleIssueIds.includes(issueId)) return moduleIssueIds;
else return uniq(concat(moduleIssueIds, [issueId])); 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 = []) => update(this.rootStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
uniq(concat(issueModuleIds, moduleIds)) uniq(concat(issueModuleIds, moduleIds))
); );
}); });
const issueToModule = await this.moduleService.addModulesToIssue(workspaceSlug, projectId, issueId, {
modules: moduleIds,
});
return issueToModule; return issueToModule;
} catch (error) { } 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; throw error;
} }
}; };
@ -407,21 +431,4 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
throw error; 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;
}
};
} }

View File

@ -1,8 +1,8 @@
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import { autorun, makeObservable, observable } from "mobx"; import { autorun, makeObservable, observable } from "mobx";
import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types";
// root store // root store
import { IWorkspaceMembership } from "@/store/member/workspace-member.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 { RootStore } from "../root.store";
import { IStateStore, StateStore } from "../state.store"; import { IStateStore, StateStore } from "../state.store";
// issues data store // issues data store