[FED-1147] chore: module link mobx integration (#2990)

* chore: link type updated

* chore: mobx implementation for module link

* chore: update module mutation logic updated and toast alert added
This commit is contained in:
Anmol Singh Bhatia 2023-12-05 19:32:25 +05:30 committed by Aaryan Khandelwal
parent 763fd4dffc
commit 97d9a40d2d
9 changed files with 219 additions and 93 deletions

View File

@ -7,12 +7,12 @@ import { Dialog, Transition } from "@headlessui/react";
// ui
import { Button, Input } from "@plane/ui";
// types
import type { IIssueLink, linkDetails, ModuleLink } from "types";
import type { IIssueLink, ILinkDetails, ModuleLink } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
data?: linkDetails | null;
data?: ILinkDetails | null;
status: boolean;
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<void>;
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<void>;

View File

@ -5,14 +5,14 @@ import { Pencil, Trash2, LinkIcon } from "lucide-react";
// helpers
import { timeAgo } from "helpers/date-time.helper";
// types
import { linkDetails, UserAuth } from "types";
import { ILinkDetails, UserAuth } from "types";
// hooks
import useToast from "hooks/use-toast";
type Props = {
links: linkDetails[];
links: ILinkDetails[];
handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: linkDetails) => void;
handleEditLink: (link: ILinkDetails) => void;
userAuth: UserAuth;
};

View File

@ -25,7 +25,7 @@ import useToast from "hooks/use-toast";
import { CustomDatePicker } from "components/ui";
import { LinkModal, LinksList } from "components/core";
// types
import { IIssue, IIssueLink, TIssuePriorities, linkDetails } from "types";
import { IIssue, IIssueLink, TIssuePriorities, ILinkDetails } from "types";
// fetch-keys
import { ISSUE_DETAILS } from "constants/fetch-keys";
// constants
@ -43,7 +43,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const { issue, issueUpdate, disableUserActions } = props;
// states
const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
const {
user: { currentProjectRole },
@ -147,7 +147,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
});
};
const handleEditLink = (link: linkDetails) => {
const handleEditLink = (link: ILinkDetails) => {
setSelectedLinkToUpdate(link);
setLinkModal(true);
};

View File

@ -37,7 +37,7 @@ import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserG
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import type { IIssue, IIssueLink, linkDetails } from "types";
import type { IIssue, IIssueLink, ILinkDetails } from "types";
// fetch-keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { EUserWorkspaceRoles } from "constants/workspace";
@ -78,7 +78,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
const {
user: userStore,
@ -244,7 +244,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
const handleEditLink = (link: linkDetails) => {
const handleEditLink = (link: ILinkDetails) => {
setSelectedLinkToUpdate(link);
setLinkModal(true);
};

View File

@ -1,13 +1,10 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr";
import { Controller, useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { ModuleService } from "services/module.service";
// hooks
import useToast from "hooks/use-toast";
// components
@ -28,9 +25,7 @@ import {
} from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { linkDetails, IModule, ModuleLink } from "types";
// fetch-keys
import { MODULE_DETAILS } from "constants/fetch-keys";
import { ILinkDetails, IModule, ModuleLink } from "types";
// constant
import { MODULE_STATUS } from "constants/module";
import { EUserWorkspaceRoles } from "constants/workspace";
@ -48,24 +43,30 @@ type Props = {
handleClose: () => void;
};
// services
const moduleService = new ModuleService();
// TODO: refactor this component
export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const { moduleId, handleClose } = props;
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, peekModule } = router.query;
const { module: moduleStore, user: userStore } = useMobxStore();
const {
module: {
moduleDetails: _moduleDetails,
updateModuleDetails,
createModuleLink,
updateModuleLink,
deleteModuleLink,
},
user: userStore,
} = useMobxStore();
const userRole = userStore.currentProjectRole;
const moduleDetails = moduleStore.moduleDetails[moduleId] ?? undefined;
const moduleDetails = _moduleDetails[moduleId] ?? undefined;
const { setToastAlert } = useToast();
@ -75,7 +76,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const submitChanges = (data: Partial<IModule>) => {
if (!workspaceSlug || !projectId || !moduleId) return;
moduleStore.updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, data);
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, data);
};
const handleCreateLink = async (formData: ModuleLink) => {
@ -83,22 +84,20 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const payload = { metadata: {}, ...formData };
await moduleService
.createModuleLink(workspaceSlug as string, projectId as string, moduleId as string, payload)
.then(() => mutate(MODULE_DETAILS(moduleId as string)))
.catch((err) => {
if (err.status === 400)
setToastAlert({
type: "error",
title: "Error!",
message: "This URL already exists for this module.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
.then(() => {
setToastAlert({
type: "success",
title: "Module link created",
message: "Module link created successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Some error occurred",
});
});
};
@ -107,50 +106,40 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const payload = { metadata: {}, ...formData };
const updatedLinks = moduleDetails.link_module.map((l) =>
l.id === linkId
? {
...l,
title: formData.title,
url: formData.url,
}
: l
);
mutate<IModule>(
MODULE_DETAILS(module.id),
(prevData) => ({ ...(prevData as IModule), link_module: updatedLinks }),
false
);
await moduleService
.updateModuleLink(workspaceSlug as string, projectId as string, module.id, linkId, payload)
updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
.then(() => {
mutate(MODULE_DETAILS(module.id));
setToastAlert({
type: "success",
title: "Module link updated",
message: "Module link updated successfully.",
});
})
.catch((err) => {
console.log(err);
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Some error occurred",
});
});
};
const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !module) return;
const updatedLinks = moduleDetails.link_module.filter((l) => l.id !== linkId);
mutate<IModule>(
MODULE_DETAILS(module.id),
(prevData) => ({ ...(prevData as IModule), link_module: updatedLinks }),
false
);
await moduleService
.deleteModuleLink(workspaceSlug as string, projectId as string, module.id, linkId)
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
.then(() => {
mutate(MODULE_DETAILS(module.id));
setToastAlert({
type: "success",
title: "Module link deleted",
message: "Module link deleted successfully.",
});
})
.catch((err) => {
console.log(err);
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Some error occurred",
});
});
};
@ -236,7 +225,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
? Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100)
: null;
const handleEditLink = (link: linkDetails) => {
const handleEditLink = (link: ILinkDetails) => {
console.log("link", link);
setSelectedLinkToUpdate(link);
setModuleLinkModal(true);

View File

@ -1,7 +1,7 @@
// services
import { APIService } from "services/api.service";
// types
import type { IModule, IIssue, IUser } from "types";
import type { IModule, IIssue, ILinkDetails } from "types";
import { IIssueResponse } from "store/issues/types";
import { API_BASE_URL } from "helpers/common.helper";
@ -132,12 +132,8 @@ export class ModuleService extends APIService {
workspaceSlug: string,
projectId: string,
moduleId: string,
data: {
metadata: any;
title: string;
url: string;
}
): Promise<any> {
data: Partial<ILinkDetails>
): Promise<ILinkDetails> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/`, data)
.then((response) => response?.data)
.catch((error) => {
@ -150,12 +146,8 @@ export class ModuleService extends APIService {
projectId: string,
moduleId: string,
linkId: string,
data: {
metadata: any;
title: string;
url: string;
}
): Promise<any> {
data: Partial<ILinkDetails>
): Promise<ILinkDetails> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`,
data

View File

@ -4,7 +4,7 @@ import { ProjectService } from "services/project";
import { ModuleService } from "services/module.service";
// types
import { RootStore } from "../root";
import { IIssue, IModule } from "types";
import { IIssue, IModule, ILinkDetails } from "types";
import {
IIssueGroupWithSubGroupsStructure,
IIssueGroupedStructure,
@ -49,6 +49,22 @@ export interface IModuleStore {
data: Partial<IModule>
) => Promise<IModule>;
deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
createModuleLink: (
workspaceSlug: string,
projectId: string,
moduleId: string,
data: Partial<ILinkDetails>
) => Promise<ILinkDetails>;
updateModuleLink: (
workspaceSlug: string,
projectId: string,
moduleId: string,
linkId: string,
data: Partial<ILinkDetails>
) => Promise<ILinkDetails>;
deleteModuleLink: (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) => Promise<void>;
addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
updateModuleGanttStructure: (
@ -119,6 +135,11 @@ export class ModuleStore implements IModuleStore {
createModule: action,
updateModuleDetails: action,
deleteModule: action,
createModuleLink: action,
updateModuleLink: action,
deleteModuleLink: action,
addModuleToFavorites: action,
removeModuleFromFavorites: action,
updateModuleGanttStructure: action,
@ -288,6 +309,130 @@ export class ModuleStore implements IModuleStore {
}
};
createModuleLink = async (
workspaceSlug: string,
projectId: string,
moduleId: string,
data: Partial<ILinkDetails>
) => {
try {
const response = await this.moduleService.createModuleLink(workspaceSlug, projectId, moduleId, data);
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: this.modules[projectId]?.map((module) =>
module.id === moduleId ? { ...module, link_module: [response, ...module.link_module] } : module
),
};
this.moduleDetails = {
...this.moduleDetails,
[moduleId]: {
...this.moduleDetails[moduleId],
link_module: [response, ...this.moduleDetails[moduleId].link_module],
},
};
});
return response;
} catch (error) {
console.error("Failed to create module link in module store", error);
this.fetchModules(workspaceSlug, projectId);
this.fetchModuleDetails(workspaceSlug, projectId, moduleId);
runInAction(() => {
this.error = error;
});
throw error;
}
};
updateModuleLink = async (
workspaceSlug: string,
projectId: string,
moduleId: string,
linkId: string,
data: Partial<ILinkDetails>
) => {
try {
const response = await this.moduleService.updateModuleLink(workspaceSlug, projectId, moduleId, linkId, data);
const _modules = {
...this.modules,
[projectId]: this.modules[projectId]?.map((module) =>
module.id === moduleId
? {
...module,
link_module: module.link_module.map((link) => (link.id === linkId ? response : link)),
}
: module
),
};
const _moduleDetails = {
...this.moduleDetails,
[moduleId]: {
...this.moduleDetails[moduleId],
link_module: this.moduleDetails[moduleId].link_module.map((link) => (link.id === linkId ? response : link)),
},
};
runInAction(() => {
this.modules = _modules;
this.moduleDetails = _moduleDetails;
});
return response;
} catch (error) {
console.error("Failed to update module link in module store", error);
this.fetchModules(workspaceSlug, projectId);
this.fetchModuleDetails(workspaceSlug, projectId, moduleId);
runInAction(() => {
this.error = error;
});
throw error;
}
};
deleteModuleLink = async (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) => {
try {
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: this.modules[projectId]?.map((module) =>
module.id === moduleId
? { ...module, link_module: module.link_module.filter((link) => link.id !== linkId) }
: module
),
};
this.moduleDetails = {
...this.moduleDetails,
[moduleId]: {
...this.moduleDetails[moduleId],
link_module: this.moduleDetails[moduleId].link_module.filter((link) => link.id !== linkId),
},
};
});
await this.moduleService.deleteModuleLink(workspaceSlug, projectId, moduleId, linkId);
} catch (error) {
console.error("Failed to delete module link in module store", error);
this.fetchModules(workspaceSlug, projectId);
this.fetchModuleDetails(workspaceSlug, projectId, moduleId);
runInAction(() => {
this.error = error;
});
throw error;
}
};
addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try {
runInAction(() => {

View File

@ -55,7 +55,7 @@ export interface IIssueLink {
url: string;
}
export interface linkDetails {
export interface ILinkDetails {
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
@ -99,7 +99,7 @@ export interface IIssue {
// tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string;
issue_cycle: IIssueCycle | null;
issue_link: linkDetails[];
issue_link: ILinkDetails[];
issue_module: IIssueModule | null;
labels: string[];
label_details: any[];

View File

@ -7,7 +7,7 @@ import type {
IWorkspaceLite,
IProjectLite,
IIssueFilterOptions,
linkDetails,
ILinkDetails,
} from "types";
export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled";
@ -29,7 +29,7 @@ export interface IModule {
id: string;
lead: string | null;
lead_detail: IUserLite | null;
link_module: linkDetails[];
link_module: ILinkDetails[];
links_list: ModuleLink[];
members: string[];
members_detail: IUserLite[];