mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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:
parent
a4d7b2423e
commit
a035cee165
@ -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>;
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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,21 +84,19 @@ 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)
|
||||
createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "This URL already exists for this module.",
|
||||
type: "success",
|
||||
title: "Module link created",
|
||||
message: "Module link created successfully.",
|
||||
});
|
||||
else
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "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 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);
|
||||
|
@ -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
|
||||
|
@ -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(() => {
|
||||
|
4
web/types/issues.d.ts
vendored
4
web/types/issues.d.ts
vendored
@ -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[];
|
||||
|
4
web/types/modules.d.ts
vendored
4
web/types/modules.d.ts
vendored
@ -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[];
|
||||
|
Loading…
Reference in New Issue
Block a user