[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 GitHub
parent 8dee7e51ca
commit aec50e2c48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 219 additions and 93 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { ProjectService } from "services/project";
import { ModuleService } from "services/module.service"; import { ModuleService } from "services/module.service";
// types // types
import { RootStore } from "../root"; import { RootStore } from "../root";
import { IIssue, IModule } from "types"; import { IIssue, IModule, ILinkDetails } from "types";
import { import {
IIssueGroupWithSubGroupsStructure, IIssueGroupWithSubGroupsStructure,
IIssueGroupedStructure, IIssueGroupedStructure,
@ -49,6 +49,22 @@ export interface IModuleStore {
data: Partial<IModule> data: Partial<IModule>
) => Promise<IModule>; ) => Promise<IModule>;
deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>; 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>; addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>; removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
updateModuleGanttStructure: ( updateModuleGanttStructure: (
@ -119,6 +135,11 @@ export class ModuleStore implements IModuleStore {
createModule: action, createModule: action,
updateModuleDetails: action, updateModuleDetails: action,
deleteModule: action, deleteModule: action,
createModuleLink: action,
updateModuleLink: action,
deleteModuleLink: action,
addModuleToFavorites: action, addModuleToFavorites: action,
removeModuleFromFavorites: action, removeModuleFromFavorites: action,
updateModuleGanttStructure: 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) => { addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try { try {
runInAction(() => { runInAction(() => {

View File

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

View File

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