diff --git a/web/components/core/modals/link-modal.tsx b/web/components/core/modals/link-modal.tsx index 02a216f06..22b082f7b 100644 --- a/web/components/core/modals/link-modal.tsx +++ b/web/components/core/modals/link-modal.tsx @@ -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; updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise; diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index b162cd34b..0dc52e170 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -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; }; diff --git a/web/components/issues/issue-peek-overview/properties.tsx b/web/components/issues/issue-peek-overview/properties.tsx index fc6a8c860..9ccba48b4 100644 --- a/web/components/issues/issue-peek-overview/properties.tsx +++ b/web/components/issues/issue-peek-overview/properties.tsx @@ -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 = observer((pro const { issue, issueUpdate, disableUserActions } = props; // states const [linkModal, setLinkModal] = useState(false); - const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); + const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); const { user: { currentProjectRole }, @@ -147,7 +147,7 @@ export const PeekOverviewProperties: FC = observer((pro }); }; - const handleEditLink = (link: linkDetails) => { + const handleEditLink = (link: ILinkDetails) => { setSelectedLinkToUpdate(link); setLinkModal(true); }; diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index 378b1e5c5..7f9a00593 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -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 = observer((props) => { const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [linkModal, setLinkModal] = useState(false); - const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); + const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); const { user: userStore, @@ -244,7 +244,7 @@ export const IssueDetailsSidebar: React.FC = 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); }; diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index f8813b80b..a257f5e1e 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -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 = observer((props) => { const { moduleId, handleClose } = props; const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); - const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); + const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(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 = observer((props) => { const submitChanges = (data: Partial) => { 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 = 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 = 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( - 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( - 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 = 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); diff --git a/web/services/module.service.ts b/web/services/module.service.ts index cbb76237f..c2db73a9b 100644 --- a/web/services/module.service.ts +++ b/web/services/module.service.ts @@ -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 { + data: Partial + ): Promise { 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 { + data: Partial + ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`, data diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts index b68c8320a..beec12ee0 100644 --- a/web/store/module/modules.store.ts +++ b/web/store/module/modules.store.ts @@ -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 ) => Promise; deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + + createModuleLink: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: Partial + ) => Promise; + updateModuleLink: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + linkId: string, + data: Partial + ) => Promise; + deleteModuleLink: (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) => Promise; + addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; 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 + ) => { + 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 + ) => { + 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(() => { diff --git a/web/types/issues.d.ts b/web/types/issues.d.ts index fdba7fd43..cedf4839b 100644 --- a/web/types/issues.d.ts +++ b/web/types/issues.d.ts @@ -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[]; diff --git a/web/types/modules.d.ts b/web/types/modules.d.ts index 6ec86c4f5..733b8f7de 100644 --- a/web/types/modules.d.ts +++ b/web/types/modules.d.ts @@ -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[];