diff --git a/web/components/core/modals/link-modal.tsx b/web/components/core/modals/link-modal.tsx index 67ba17a7d..f80b58d33 100644 --- a/web/components/core/modals/link-modal.tsx +++ b/web/components/core/modals/link-modal.tsx @@ -14,8 +14,8 @@ type Props = { handleClose: () => void; data?: ILinkDetails | null; status: boolean; - createIssueLink: (formData: IIssueLink | ModuleLink) => Promise; - updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise; + createIssueLink: (formData: IIssueLink | ModuleLink) => Promise | Promise | void; + updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise | Promise | void; }; const defaultValues: IIssueLink | ModuleLink = { @@ -31,7 +31,7 @@ export const LinkModal: FC = (props) => { handleSubmit, control, reset, - } = useForm({ + } = useForm({ defaultValues, }); @@ -158,8 +158,8 @@ export const LinkModal: FC = (props) => { ? "Updating Link..." : "Update Link" : isSubmitting - ? "Adding Link..." - : "Add Link"} + ? "Adding Link..." + : "Add Link"} diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 7fa50e07c..25aaf2f88 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -1,5 +1,5 @@ import { FC, useState } from "react"; -import { mutate } from "swr"; + import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // mobx store @@ -17,30 +17,25 @@ import { SidebarPrioritySelect, SidebarStateSelect, } from "../sidebar-select"; -// services -import { IssueService } from "services/issue"; -// hooks -import useToast from "hooks/use-toast"; // components import { CustomDatePicker } from "components/ui"; import { LinkModal, LinksList } from "components/core"; // types -import { IIssue, IIssueLink, TIssuePriorities, ILinkDetails } from "types"; -// fetch-keys -import { ISSUE_DETAILS } from "constants/fetch-keys"; +import { IIssue, TIssuePriorities, ILinkDetails, IIssueLink } from "types"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; interface IPeekOverviewProperties { issue: IIssue; issueUpdate: (issue: Partial) => void; + issueLinkCreate: (data: IIssueLink) => Promise; + issueLinkUpdate: (data: IIssueLink, linkId: string) => Promise; + issueLinkDelete: (linkId: string) => Promise; disableUserActions: boolean; } -const issueService = new IssueService(); - export const PeekOverviewProperties: FC = observer((props) => { - const { issue, issueUpdate, disableUserActions } = props; + const { issue, issueUpdate, issueLinkCreate, issueLinkUpdate, issueLinkDelete, disableUserActions } = props; // states const [linkModal, setLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); @@ -54,8 +49,6 @@ export const PeekOverviewProperties: FC = observer((pro const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); - const handleState = (_state: string) => { issueUpdate({ ...issue, state: _state }); }; @@ -81,61 +74,6 @@ export const PeekOverviewProperties: FC = observer((pro issueUpdate({ ...issue, ...formData }); }; - const handleCreateLink = async (formData: IIssueLink) => { - if (!workspaceSlug || !projectId || !issue) return; - - const payload = { metadata: {}, ...formData }; - - await issueService - .createIssueLink(workspaceSlug as string, projectId as string, issue.id, payload) - .then(() => mutate(ISSUE_DETAILS(issue.id))) - .catch((err) => { - if (err.status === 400) - setToastAlert({ - type: "error", - title: "Error!", - message: "This URL already exists for this issue.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: "Something went wrong. Please try again.", - }); - }); - }; - - const handleUpdateLink = async (formData: IIssueLink, linkId: string) => { - if (!workspaceSlug || !projectId || !issue) return; - - const payload = { metadata: {}, ...formData }; - - const updatedLinks = issue.issue_link.map((l) => - l.id === linkId - ? { - ...l, - title: formData.title, - url: formData.url, - } - : l - ); - - mutate( - ISSUE_DETAILS(issue.id), - (prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }), - false - ); - - await issueService - .updateIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId, payload) - .then(() => { - mutate(ISSUE_DETAILS(issue.id)); - }) - .catch((err) => { - console.log(err); - }); - }; - const handleCycleOrModuleChange = async () => { if (!workspaceSlug || !projectId) return; @@ -147,27 +85,6 @@ export const PeekOverviewProperties: FC = observer((pro setLinkModal(true); }; - const handleDeleteLink = async (linkId: string) => { - if (!workspaceSlug || !projectId || !issue) return; - - const updatedLinks = issue.issue_link.filter((l) => l.id !== linkId); - - mutate( - ISSUE_DETAILS(issue.id), - (prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }), - false - ); - - await issueService - .deleteIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId) - .then(() => { - mutate(ISSUE_DETAILS(issue.id)); - }) - .catch((err) => { - console.log(err); - }); - }; - const projectDetails = workspaceSlug ? getProjectById(workspaceSlug.toString(), issue.project) : null; const isEstimateEnabled = projectDetails?.estimate; @@ -187,8 +104,8 @@ export const PeekOverviewProperties: FC = observer((pro }} data={selectedLinkToUpdate} status={selectedLinkToUpdate ? true : false} - createIssueLink={handleCreateLink} - updateIssueLink={handleUpdateLink} + createIssueLink={issueLinkCreate} + updateIssueLink={issueLinkUpdate} />
@@ -373,7 +290,7 @@ export const PeekOverviewProperties: FC = observer((pro {issue?.issue_link && issue.issue_link.length > 0 ? ( = observer((props) => { removeIssueReaction, createIssueSubscription, removeIssueSubscription, + createIssueLink, + updateIssueLink, + deleteIssueLink, getIssue, loader, fetchPeekIssueDetails, @@ -121,6 +124,13 @@ export const IssuePeekOverview: FC = observer((props) => { const issueSubscriptionRemove = () => removeIssueSubscription(workspaceSlug, projectId, issueId); + const issueLinkCreate = (formData: IIssueLink) => createIssueLink(workspaceSlug, projectId, issueId, formData); + + const issueLinkUpdate = (formData: IIssueLink, linkId: string) => + updateIssueLink(workspaceSlug, projectId, issueId, linkId, formData); + + const issueLinkDelete = (linkId: string) => deleteIssueLink(workspaceSlug, projectId, issueId, linkId); + const handleDeleteIssue = async () => { if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!); else removeIssueFromStructure(workspaceSlug, projectId, issue!); @@ -159,6 +169,9 @@ export const IssuePeekOverview: FC = observer((props) => { issueCommentReactionRemove={issueCommentReactionRemove} issueSubscriptionCreate={issueSubscriptionCreate} issueSubscriptionRemove={issueSubscriptionRemove} + issueLinkCreate={issueLinkCreate} + issueLinkUpdate={issueLinkUpdate} + issueLinkDelete={issueLinkDelete} handleDeleteIssue={handleDeleteIssue} disableUserActions={[5, 10].includes(userRole)} showCommentAccessSpecifier={currentProjectDetails?.is_deployed} diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index 33ea1bcfb..8294784f8 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -17,7 +17,7 @@ import { // ui import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui"; // types -import { IIssue } from "types"; +import { IIssue, IIssueLink, ILinkDetails } from "types"; interface IIssueView { workspaceSlug: string; @@ -38,6 +38,9 @@ interface IIssueView { issueCommentReactionRemove: (commentId: string, reaction: string) => void; issueSubscriptionCreate: () => void; issueSubscriptionRemove: () => void; + issueLinkCreate: (formData: IIssueLink) => Promise; + issueLinkUpdate: (formData: IIssueLink, linkId: string) => Promise; + issueLinkDelete: (linkId: string) => Promise; handleDeleteIssue: () => Promise; children: ReactNode; disableUserActions?: boolean; @@ -84,6 +87,9 @@ export const IssueView: FC = observer((props) => { issueCommentReactionRemove, issueSubscriptionCreate, issueSubscriptionRemove, + issueLinkCreate, + issueLinkUpdate, + issueLinkDelete, handleDeleteIssue, children, disableUserActions = false, @@ -286,6 +292,9 @@ export const IssueView: FC = observer((props) => { @@ -342,6 +351,9 @@ export const IssueView: FC = observer((props) => {
diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index 60b6106e3..f002b6dda 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -84,6 +84,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { user: { currentUser, currentProjectRole }, projectState: { states }, projectIssues: { removeIssue }, + issueDetail: { createIssueLink, updateIssueLink, deleteIssueLink }, } = useMobxStore(); const router = useRouter(); @@ -129,80 +130,19 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { [workspaceSlug, projectId, issueId, issueDetail, currentUser] ); - const handleCreateLink = async (formData: IIssueLink) => { - if (!workspaceSlug || !projectId || !issueDetail) return; - - const payload = { metadata: {}, ...formData }; - - await issueService - .createIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, payload) - .then(() => mutate(ISSUE_DETAILS(issueDetail.id))) - .catch((err) => { - if (err.status === 400) - setToastAlert({ - type: "error", - title: "Error!", - message: "This URL already exists for this issue.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: "Something went wrong. Please try again.", - }); - }); + const issueLinkCreate = (formData: IIssueLink) => { + if (!workspaceSlug || !projectId || !issueId) return; + createIssueLink(workspaceSlug.toString(), projectId.toString(), issueId.toString(), formData); }; - const handleUpdateLink = async (formData: IIssueLink, linkId: string) => { - if (!workspaceSlug || !projectId || !issueDetail) return; - - const payload = { metadata: {}, ...formData }; - - const updatedLinks = issueDetail.issue_link.map((l) => - l.id === linkId - ? { - ...l, - title: formData.title, - url: formData.url, - } - : l - ); - - mutate( - ISSUE_DETAILS(issueDetail.id), - (prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }), - false - ); - - await issueService - .updateIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, linkId, payload) - .then(() => { - mutate(ISSUE_DETAILS(issueDetail.id)); - }) - .catch((err) => { - console.log(err); - }); + const issueLinkUpdate = (formData: IIssueLink, linkId: string) => { + if (!workspaceSlug || !projectId || !issueId) return; + updateIssueLink(workspaceSlug.toString(), projectId.toString(), issueId.toString(), linkId, formData); }; - const handleDeleteLink = async (linkId: string) => { - if (!workspaceSlug || !projectId || !issueDetail) return; - - const updatedLinks = issueDetail.issue_link.filter((l) => l.id !== linkId); - - mutate( - ISSUE_DETAILS(issueDetail.id), - (prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }), - false - ); - - await issueService - .deleteIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, linkId) - .then(() => { - mutate(ISSUE_DETAILS(issueDetail.id)); - }) - .catch((err) => { - console.log(err); - }); + const issueLinkDelete = (linkId: string) => { + if (!workspaceSlug || !projectId || !issueId) return; + deleteIssueLink(workspaceSlug.toString(), projectId.toString(), issueId.toString(), linkId); }; const handleCopyText = () => { @@ -264,8 +204,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { }} data={selectedLinkToUpdate} status={selectedLinkToUpdate ? true : false} - createIssueLink={handleCreateLink} - updateIssueLink={handleUpdateLink} + createIssueLink={issueLinkCreate} + updateIssueLink={issueLinkUpdate} /> {workspaceSlug && projectId && issueDetail && ( = observer((props) => { { { + data: IIssueLink + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`, data) .then((response) => response?.data) .catch((error) => { @@ -202,12 +198,8 @@ export class IssueService extends APIService { projectId: string, issueId: string, linkId: string, - data: { - metadata: any; - title: string; - url: string; - } - ): Promise { + data: IIssueLink + ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`, data diff --git a/web/services/module.service.ts b/web/services/module.service.ts index c2db73a9b..9f8e73c54 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, ILinkDetails } from "types"; +import type { IModule, IIssue, ILinkDetails, ModuleLink } from "types"; import { IIssueResponse } from "store/issues/types"; import { API_BASE_URL } from "helpers/common.helper"; @@ -132,7 +132,7 @@ export class ModuleService extends APIService { workspaceSlug: string, projectId: string, moduleId: string, - data: Partial + data: ModuleLink ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/`, data) .then((response) => response?.data) @@ -146,7 +146,7 @@ export class ModuleService extends APIService { projectId: string, moduleId: string, linkId: string, - data: Partial + data: ModuleLink ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`, diff --git a/web/store/issue/issue_detail.store.ts b/web/store/issue/issue_detail.store.ts index 07e7f0c78..fa4651be7 100644 --- a/web/store/issue/issue_detail.store.ts +++ b/web/store/issue/issue_detail.store.ts @@ -4,7 +4,7 @@ import { IssueService, IssueReactionService, IssueCommentService } from "service import { NotificationService } from "services/notification.service"; // types import { RootStore } from "../root"; -import type { IIssue, IIssueActivity } from "types"; +import type { IIssue, IIssueActivity, IIssueLink, ILinkDetails } from "types"; // constants import { groupReactionEmojis } from "constants/issue"; @@ -51,6 +51,21 @@ export interface IIssueDetailStore { createIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise; removeIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise; + createIssueLink: ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: IIssueLink + ) => Promise; + updateIssueLink: ( + workspaceSlug: string, + projectId: string, + issueId: string, + linkId: string, + data: IIssueLink + ) => Promise; + deleteIssueLink: (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => Promise; + fetchIssueActivity: (workspaceSlug: string, projectId: string, issueId: string) => Promise; createIssueComment: (workspaceSlug: string, projectId: string, issueId: string, data: any) => Promise; updateIssueComment: ( @@ -147,6 +162,10 @@ export class IssueDetailStore implements IIssueDetailStore { createIssueReaction: action, removeIssueReaction: action, + createIssueLink: action, + updateIssueLink: action, + deleteIssueLink: action, + fetchIssueActivity: action, createIssueComment: action, updateIssueComment: action, @@ -590,6 +609,91 @@ export class IssueDetailStore implements IIssueDetailStore { } }; + createIssueLink = async (workspaceSlug: string, projectId: string, issueId: string, data: IIssueLink) => { + try { + const response = await this.issueService.createIssueLink(workspaceSlug, projectId, issueId, data); + + runInAction(() => { + this.issues = { + ...this.issues, + [issueId]: { + ...this.issues[issueId], + issue_link: [response, ...this.issues[issueId].issue_link], + }, + }; + }); + + return response; + } catch (error) { + console.error("Failed to create link in store", error); + + this.fetchIssueDetails(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateIssueLink = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + linkId: string, + data: IIssueLink + ) => { + try { + const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data); + + runInAction(() => { + this.issues = { + ...this.issues, + [issueId]: { + ...this.issues[issueId], + issue_link: this.issues[issueId].issue_link.map((link) => (link.id === linkId ? response : link)), + }, + }; + }); + + return response; + } catch (error) { + console.error("Failed to update link in issue store", error); + + this.fetchIssueDetails(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteIssueLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => { + try { + runInAction(() => { + this.issues = { + ...this.issues, + [issueId]: { + ...this.issues[issueId], + issue_link: this.issues[issueId].issue_link.filter((link) => link.id !== linkId), + }, + }; + }); + await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId); + } catch (error) { + console.error("Failed to delete link in issue store", error); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + // subscriptions fetchIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => { try { diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts index beec12ee0..359cdb2da 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, ILinkDetails } from "types"; +import { IIssue, IModule, ILinkDetails, ModuleLink } from "types"; import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, @@ -54,14 +54,14 @@ export interface IModuleStore { workspaceSlug: string, projectId: string, moduleId: string, - data: Partial + data: ModuleLink ) => Promise; updateModuleLink: ( workspaceSlug: string, projectId: string, moduleId: string, linkId: string, - data: Partial + data: ModuleLink ) => Promise; deleteModuleLink: (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) => Promise; @@ -309,12 +309,7 @@ export class ModuleStore implements IModuleStore { } }; - createModuleLink = async ( - workspaceSlug: string, - projectId: string, - moduleId: string, - data: Partial - ) => { + createModuleLink = async (workspaceSlug: string, projectId: string, moduleId: string, data: ModuleLink) => { try { const response = await this.moduleService.createModuleLink(workspaceSlug, projectId, moduleId, data); @@ -354,7 +349,7 @@ export class ModuleStore implements IModuleStore { projectId: string, moduleId: string, linkId: string, - data: Partial + data: ModuleLink ) => { try { const response = await this.moduleService.updateModuleLink(workspaceSlug, projectId, moduleId, linkId, data);