diff --git a/web/components/headers/index.ts b/web/components/headers/index.ts index e9a6ad5f0..0657b5ae3 100644 --- a/web/components/headers/index.ts +++ b/web/components/headers/index.ts @@ -10,7 +10,7 @@ export * from "./workspace-dashboard"; export * from "./projects"; export * from "./profile-preferences"; export * from "./cycles"; -export * from "./modules"; +export * from "./modules-list"; export * from "./project-settings"; export * from "./workspace-settings"; export * from "./pages"; diff --git a/web/components/headers/modules.tsx b/web/components/headers/modules-list.tsx similarity index 78% rename from web/components/headers/modules.tsx rename to web/components/headers/modules-list.tsx index 805a26029..8d5df1b32 100644 --- a/web/components/headers/modules.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,20 +1,17 @@ -import { Dispatch, FC, SetStateAction } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; +import { observer } from "mobx-react-lite"; import { Plus } from "lucide-react"; -// components +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useLocalStorage from "hooks/use-local-storage"; // ui import { Breadcrumbs, BreadcrumbItem, Button, Tooltip } from "@plane/ui"; import { Icon } from "components/ui"; // helper import { replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper"; -export interface IModulesHeader { - name: string | undefined; - modulesView: string; - setModulesView: Dispatch>; -} - const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [ { type: "gantt_chart", @@ -26,11 +23,15 @@ const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [ }, ]; -export const ModulesHeader: FC = (props) => { - const { name, modulesView, setModulesView } = props; +export const ModulesListHeader: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; + + const { project: projectStore } = useMobxStore(); + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined; + + const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid"); return (
= (props) => { } /> - +
@@ -83,4 +84,4 @@ export const ModulesHeader: FC = (props) => { ); -}; +}); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 98e58600b..8dc826300 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -119,7 +119,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { setDeleteIssueModal(true); }} > -
+
Delete issue
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 7f37285b9..066a16d35 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -119,7 +119,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { setDeleteIssueModal(true); }} > -
+
Delete issue
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 00005a02e..0cad8c46f 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -106,7 +106,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => { setDeleteIssueModal(true); }} > -
+
Delete issue
diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index 3d8a53139..c3abcf182 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -6,8 +6,7 @@ import { Dialog, Transition } from "@headlessui/react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // services -import { ModuleService } from "services/module.service"; -import { IssueService, IssueDraftService } from "services/issue"; +import { IssueDraftService } from "services/issue"; // hooks import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; @@ -40,8 +39,6 @@ export interface IssuesModalProps { onSubmit?: (data: Partial) => Promise; } -const moduleService = new ModuleService(); -const issueService = new IssueService(); const issueDraftService = new IssueDraftService(); export const CreateUpdateIssueModal: React.FC = observer((props) => { @@ -170,35 +167,15 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }, [activeProject, data, projectId, projects, isOpen]); const addIssueToCycle = async (issueId: string, cycleId: string) => { - if (!workspaceSlug || !activeProject || !user) return; + if (!workspaceSlug || !activeProject) return; - await issueService - .addIssueToCycle( - workspaceSlug.toString(), - activeProject, - cycleId, - { - issues: [issueId], - }, - user - ) - .then(() => cycleIssueStore.fetchIssues(workspaceSlug.toString(), activeProject, cycleId)); + cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), activeProject, cycleId, issueId); }; const addIssueToModule = async (issueId: string, moduleId: string) => { - if (!workspaceSlug || !activeProject || !user) return; + if (!workspaceSlug || !activeProject) return; - await moduleService - .addIssuesToModule( - workspaceSlug.toString(), - activeProject, - moduleId, - { - issues: [issueId], - }, - user - ) - .then(() => moduleIssueStore.fetchIssues(workspaceSlug.toString(), activeProject, moduleId)); + moduleIssueStore.addIssueToModule(workspaceSlug.toString(), activeProject, moduleId, issueId); }; const createIssue = async (payload: Partial) => { @@ -267,10 +244,10 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }; const updateIssue = async (payload: Partial) => { - if (!user) return; + if (!workspaceSlug || !activeProject || !data) return; - await issueService - .patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user) + await issueDetailStore + .updateIssue(workspaceSlug.toString(), activeProject, data.id, payload) .then(() => { if (!createMore) onFormSubmitClose(); diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx index 6d06c493d..3cb5942ee 100644 --- a/web/components/modules/delete-module-modal.tsx +++ b/web/components/modules/delete-module-modal.tsx @@ -1,13 +1,6 @@ import React, { useState } from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui import { Dialog, Transition } from "@headlessui/react"; -// services -import { ModuleService } from "services/module.service"; // hooks import useToast from "hooks/use-toast"; // ui @@ -15,42 +8,40 @@ import { Button } from "@plane/ui"; // icons import { AlertTriangle } from "lucide-react"; // types -import type { IUser, IModule } from "types"; -// fetch-keys -import { MODULE_LIST } from "constants/fetch-keys"; +import type { IModule } from "types"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { + data: IModule; isOpen: boolean; - setIsOpen: React.Dispatch>; - data?: IModule; - user: IUser | undefined; + onClose: () => void; }; -// services -const moduleService = new ModuleService(); +export const DeleteModuleModal: React.FC = observer((props) => { + const { data, isOpen, onClose } = props; -export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; + const { module: moduleStore } = useMobxStore(); + const { setToastAlert } = useToast(); const handleClose = () => { - setIsOpen(false); + onClose(); setIsDeleteLoading(false); }; const handleDeletion = async () => { + if (!workspaceSlug || !projectId) return; + setIsDeleteLoading(true); - if (!workspaceSlug || !projectId || !data) return; - - mutate(MODULE_LIST(projectId as string), (prevData) => prevData?.filter((m) => m.id !== data.id), false); - - await moduleService - .deleteModule(workspaceSlug as string, projectId as string, data.id, user) + await moduleStore + .deleteModule(workspaceSlug.toString(), projectId.toString(), data.id) .then(() => { if (moduleId) router.push(`/${workspaceSlug}/projects/${data.project}/modules`); handleClose(); @@ -61,6 +52,8 @@ export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data, us title: "Error!", message: "Module could not be deleted. Please try again.", }); + }) + .finally(() => { setIsDeleteLoading(false); }); }; @@ -126,4 +119,4 @@ export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data, us ); -}; +}); diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index f25b76921..2a6d18834 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -1,65 +1,26 @@ -import { FC } from "react"; - import { useRouter } from "next/router"; - -import { KeyedMutator } from "swr"; - -// services -import { ModuleService } from "services/module.service"; -// hooks -import useUser from "hooks/use-user"; -import useProjectDetails from "hooks/use-project-details"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { GanttChartRoot, IBlockUpdateData } from "components/gantt-chart"; import { ModuleGanttBlock, ModuleGanttSidebarBlock } from "components/modules"; // types import { IModule } from "types"; -type Props = { - modules: IModule[]; - mutateModules: KeyedMutator; -}; - -// services -const moduleService = new ModuleService(); - -export const ModulesListGanttChartView: FC = ({ modules, mutateModules }) => { +export const ModulesListGanttChartView: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; - const { user } = useUser(); - const { projectDetails } = useProjectDetails(); + const { project: projectStore, module: moduleStore } = useMobxStore(); + + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined; + const modules = moduleStore.projectModules; const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => { - if (!workspaceSlug || !user) return; + if (!workspaceSlug) return; - mutateModules((prevData: any) => { - if (!prevData) return prevData; - - const newList = prevData.map((p: any) => ({ - ...p, - ...(p.id === module.id - ? { - start_date: payload.start_date ? payload.start_date : p.start_date, - target_date: payload.target_date ? payload.target_date : p.target_date, - sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order, - } - : {}), - })); - - if (payload.sort_order) { - const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0]; - newList.splice(payload.sort_order.destinationIndex, 0, removedElement); - } - - return newList; - }, false); - - const newPayload: any = { ...payload }; - - if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; - - moduleService.patchModule(workspaceSlug.toString(), module.project, module.id, newPayload, user); + moduleStore.updateModuleGanttStructure(workspaceSlug.toString(), module.project, module, payload); }; const blockFormat = (blocks: IModule[]) => @@ -93,4 +54,4 @@ export const ModulesListGanttChartView: FC = ({ modules, mutateModules }) />
); -}; +}); diff --git a/web/components/modules/index.ts b/web/components/modules/index.ts index 2a7f54fb3..750db2fd0 100644 --- a/web/components/modules/index.ts +++ b/web/components/modules/index.ts @@ -4,5 +4,6 @@ export * from "./delete-module-modal"; export * from "./form"; export * from "./gantt-chart"; export * from "./modal"; +export * from "./modules-list-view"; export * from "./sidebar"; -export * from "./single-module-card"; +export * from "./module-card-item"; diff --git a/web/components/modules/modal.tsx b/web/components/modules/modal.tsx index 282866bf1..cfb422cbf 100644 --- a/web/components/modules/modal.tsx +++ b/web/components/modules/modal.tsx @@ -1,18 +1,15 @@ import React, { useEffect, useState } from "react"; -import { mutate } from "swr"; +import { observer } from "mobx-react-lite"; import { useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { ModuleForm } from "components/modules"; -// services -import { ModuleService } from "services/module.service"; // hooks import useToast from "hooks/use-toast"; // types -import type { IUser, IModule } from "types"; -// fetch-keys -import { MODULE_LIST } from "constants/fetch-keys"; -import { useMobxStore } from "lib/mobx/store-provider"; +import type { IModule } from "types"; type Props = { isOpen: boolean; @@ -30,14 +27,12 @@ const defaultValues: Partial = { members_list: [], }; -const moduleService = new ModuleService(); - -export const CreateUpdateModuleModal: React.FC = (props) => { +export const CreateUpdateModuleModal: React.FC = observer((props) => { const { isOpen, onClose, data, workspaceSlug, projectId } = props; const [activeProject, setActiveProject] = useState(null); - const { project: projectStore } = useMobxStore(); + const { project: projectStore, module: moduleStore } = useMobxStore(); const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; @@ -53,10 +48,11 @@ export const CreateUpdateModuleModal: React.FC = (props) => { }); const createModule = async (payload: Partial) => { - await moduleService - .createModule(workspaceSlug as string, projectId as string, payload, {} as IUser) + if (!workspaceSlug || !projectId) return; + + await moduleStore + .createModule(workspaceSlug.toString(), projectId.toString(), payload) .then(() => { - mutate(MODULE_LIST(projectId as string)); handleClose(); setToastAlert({ @@ -75,19 +71,11 @@ export const CreateUpdateModuleModal: React.FC = (props) => { }; const updateModule = async (payload: Partial) => { - await moduleService - .updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload, {} as IUser) - .then((res) => { - mutate( - MODULE_LIST(projectId as string), - (prevData) => - prevData?.map((p) => { - if (p.id === res.id) return { ...p, ...payload }; + if (!workspaceSlug || !projectId || !data) return; - return p; - }), - false - ); + await moduleStore + .updateModuleDetails(workspaceSlug.toString(), projectId.toString(), data.id, payload) + .then(() => { handleClose(); setToastAlert({ @@ -180,4 +168,4 @@ export const CreateUpdateModuleModal: React.FC = (props) => { ); -}; +}); diff --git a/web/components/modules/single-module-card.tsx b/web/components/modules/module-card-item.tsx similarity index 69% rename from web/components/modules/single-module-card.tsx rename to web/components/modules/module-card-item.tsx index bf1a304d8..74dd20ad9 100644 --- a/web/components/modules/single-module-card.tsx +++ b/web/components/modules/module-card-item.tsx @@ -1,35 +1,32 @@ import React, { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { mutate } from "swr"; -// services -import { ModuleService } from "services/module.service"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // components -import { DeleteModuleModal } from "components/modules"; +import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; // ui import { AssigneesList } from "components/ui"; import { CustomMenu, Tooltip } from "@plane/ui"; // icons import { CalendarDays, LinkIcon, Pencil, Star, Target, Trash2 } from "lucide-react"; // helpers -import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +import { copyUrlToClipboard, truncateText } from "helpers/string.helper"; import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; // types -import { IUser, IModule } from "types"; -// fetch-key -import { MODULE_LIST } from "constants/fetch-keys"; +import { IModule } from "types"; type Props = { module: IModule; - handleEditModule: () => void; - user: IUser | undefined; }; -const moduleService = new ModuleService(); +export const ModuleCardItem: React.FC = observer((props) => { + const { module } = props; -export const SingleModuleCard: React.FC = ({ module, handleEditModule, user }) => { + const [editModuleModal, setEditModuleModal] = useState(false); const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const router = useRouter(); @@ -37,54 +34,26 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule, us const { setToastAlert } = useToast(); + const { module: moduleStore } = useMobxStore(); + const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100; - const handleDeleteModule = () => { - if (!module) return; - - setModuleDeleteModal(true); - }; - const handleAddToFavorites = () => { - if (!workspaceSlug || !projectId || !module) return; + if (!workspaceSlug || !projectId) return; - mutate( - MODULE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((m) => ({ - ...m, - is_favorite: m.id === module.id ? true : m.is_favorite, - })), - false - ); - - moduleService - .addModuleToFavorites(workspaceSlug as string, projectId as string, { - module: module.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", - }); + moduleStore.addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the module to favorites. Please try again.", }); + }); }; const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !projectId || !module) return; + if (!workspaceSlug || !projectId) return; - mutate( - MODULE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((m) => ({ - ...m, - is_favorite: m.id === module.id ? false : m.is_favorite, - })), - false - ); - - moduleService.removeModuleFromFavorites(workspaceSlug as string, projectId as string, module.id).catch(() => { + moduleStore.removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -94,9 +63,7 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule, us }; const handleCopyText = () => { - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => { + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", @@ -111,7 +78,16 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule, us return ( <> - + {workspaceSlug && projectId && ( + setEditModuleModal(false)} + data={module} + projectId={projectId.toString()} + workspaceSlug={workspaceSlug.toString()} + /> + )} + setModuleDeleteModal(false)} />
@@ -140,25 +116,25 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule, us )} - - + + - + + Copy link + + + setEditModuleModal(true)}> + + Edit module - + setModuleDeleteModal(true)}> - + Delete module - - - - Copy module link - -
@@ -204,4 +180,4 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule, us
); -}; +}); diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx new file mode 100644 index 000000000..d40b72a31 --- /dev/null +++ b/web/components/modules/modules-list-view.tsx @@ -0,0 +1,68 @@ +import { observer } from "mobx-react-lite"; +import { Plus } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useLocalStorage from "hooks/use-local-storage"; +// components +import { ModuleCardItem, ModulesListGanttChartView } from "components/modules"; +import { EmptyState } from "components/common"; +// ui +import { Loader } from "@plane/ui"; +// assets +import emptyModule from "public/empty-state/module.svg"; + +export const ModulesListView: React.FC = observer(() => { + const { module: moduleStore } = useMobxStore(); + + const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); + + const modulesList = moduleStore.projectModules; + + if (!modulesList) + return ( + + + + + + + + + ); + + return ( + <> + {modulesList.length > 0 ? ( + <> + {modulesView === "grid" && ( +
+
+ {modulesList.map((module) => ( + + ))} +
+
+ )} + {modulesView === "gantt_chart" && } + + ) : ( + , + text: "New Module", + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "m", + }); + document.dispatchEvent(e); + }, + }} + /> + )} + + ); +}); diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 095fc9722..c6a2818ee 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -228,7 +228,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { createIssueLink={handleCreateLink} updateIssueLink={handleUpdateLink} /> - + setModuleDeleteModal(false)} data={moduleDetails} />
= observer((props) => { className="hidden group-hover:block flex-shrink-0" buttonClassName="!text-custom-sidebar-text-400 hover:text-custom-sidebar-text-400" ellipsis + placement="bottom-start" > {!shortContextMenu && isAdmin && ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 762f1e281..fd6d05f8e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -16,8 +16,10 @@ import { ModuleIssuesHeader } from "components/headers"; import { EmptyState } from "components/common"; // assets import emptyModule from "public/empty-state/module.svg"; +// types +import { NextPage } from "next"; -const SingleModule: React.FC = () => { +const ModuleIssuesPage: NextPage = () => { const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const router = useRouter(); @@ -91,4 +93,4 @@ const SingleModule: React.FC = () => { ); }; -export default SingleModule; +export default ModuleIssuesPage; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 5bac70249..9ef9b13c4 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -1,133 +1,15 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; +import React from "react"; import { NextPage } from "next"; // layouts import { AppLayout } from "layouts/app-layout"; -// hooks -import useUserAuth from "hooks/use-user-auth"; -// services -import { ProjectService } from "services/project"; -import { ModuleService } from "services/module.service"; // components -import { CreateUpdateModuleModal, ModulesListGanttChartView, SingleModuleCard } from "components/modules"; -import { ModulesHeader } from "components/headers"; -// ui -import { Loader } from "@plane/ui"; -import { EmptyState } from "components/common"; -// icons -import { Plus } from "lucide-react"; -// images -import emptyModule from "public/empty-state/module.svg"; -// types -import { IModule, SelectModuleType } from "types/modules"; -// fetch-keys -import { MODULE_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; +import { ModulesListView } from "components/modules"; +import { ModulesListHeader } from "components/headers"; -// services -const projectService = new ProjectService(); -const moduleService = new ModuleService(); - -const ProjectModules: NextPage = () => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - // states - const [modulesView, setModulesView] = useState<"grid" | "gantt_chart">("grid"); - const [selectedModule, setSelectedModule] = useState(); - const [createUpdateModule, setCreateUpdateModule] = useState(false); - - const { user } = useUserAuth(); - - const { data: activeProject } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null - ); - - const { data: modules, mutate: mutateModules } = useSWR( - workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, - workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null - ); - - const handleEditModule = (module: IModule) => { - setSelectedModule({ ...module, actionType: "edit" }); - setCreateUpdateModule(true); - }; - - useEffect(() => { - if (createUpdateModule) return; - - const timer = setTimeout(() => { - setSelectedModule(undefined); - clearTimeout(timer); - }, 500); - }, [createUpdateModule]); - - return ( - } - > - {workspaceSlug && projectId && ( - { - setCreateUpdateModule(false); - }} - data={selectedModule} - workspaceSlug={workspaceSlug.toString()} - projectId={projectId.toString()} - /> - )} - - {modules ? ( - modules.length > 0 ? ( - <> - {modulesView === "grid" && ( -
-
- {modules.map((module) => ( - handleEditModule(module)} - user={user} - /> - ))} -
-
- )} - {modulesView === "gantt_chart" && ( - - )} - - ) : ( - , - text: "New Module", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "m", - }); - document.dispatchEvent(e); - }, - }} - /> - ) - ) : ( - - - - - - - - - )} -
- ); -}; +const ProjectModules: NextPage = () => ( + } withProjectWrapper> + + +); export default ProjectModules; diff --git a/web/services/module.service.ts b/web/services/module.service.ts index 60763122e..dec4b2d8f 100644 --- a/web/services/module.service.ts +++ b/web/services/module.service.ts @@ -61,11 +61,11 @@ export class ModuleService extends APIService { projectId: string, moduleId: string, data: Partial, - user: any - ): Promise { + user: IUser | undefined + ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data) .then((response) => { - trackEventService.trackModuleEvent(response?.data, "MODULE_UPDATE", user); + if (user) trackEventService.trackModuleEvent(response?.data, "MODULE_UPDATE", user); return response?.data; }) .catch((error) => { diff --git a/web/store/issue/issue_detail.store.ts b/web/store/issue/issue_detail.store.ts index 7b4a2be86..bee6fdedc 100644 --- a/web/store/issue/issue_detail.store.ts +++ b/web/store/issue/issue_detail.store.ts @@ -38,7 +38,7 @@ export interface IIssueDetailStore { // creating issue createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; // updating issue - updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial) => void; + updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial) => Promise; // deleting issue deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -260,6 +260,8 @@ export class IssueDetailStore implements IIssueDetailStore { }, }; }); + + return response; } catch (error) { this.fetchIssueDetails(workspaceSlug, projectId, issueId); diff --git a/web/store/module/module_filters.store.ts b/web/store/module/module_filters.store.ts index 66ecb7278..ae94af59e 100644 --- a/web/store/module/module_filters.store.ts +++ b/web/store/module/module_filters.store.ts @@ -152,6 +152,8 @@ export class ModuleFilterStore implements IModuleFilterStore { this.moduleFilters = newFilters; }); + const user = this.rootStore.user.currentUser ?? undefined; + this.moduleService.patchModule( workspaceSlug, projectId, @@ -161,7 +163,7 @@ export class ModuleFilterStore implements IModuleFilterStore { filters: newFilters, }, }, - this.rootStore.user.currentUser + user ); } catch (error) { this.fetchModuleFilters(workspaceSlug, projectId, moduleId); diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts index da641ece9..91a11cd76 100644 --- a/web/store/module/modules.store.ts +++ b/web/store/module/modules.store.ts @@ -10,6 +10,7 @@ import { IIssueGroupedStructure, IIssueUnGroupedStructure, } from "../issue/issue.store"; +import { IBlockUpdateData } from "components/gantt-chart"; export interface IModuleStore { // states @@ -41,10 +42,21 @@ export interface IModuleStore { fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise; - updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => void; - deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => void; - addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void; - removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void; + updateModuleDetails: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: Partial + ) => Promise; + deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateModuleGanttStructure: ( + workspaceSlug: string, + projectId: string, + module: IModule, + payload: IBlockUpdateData + ) => void; // computed projectModules: IModule[] | null; @@ -109,6 +121,7 @@ export class ModuleStore implements IModuleStore { deleteModule: action, addModuleToFavorites: action, removeModuleFromFavorites: action, + updateModuleGanttStructure: action, // computed projectModules: computed, @@ -242,7 +255,11 @@ export class ModuleStore implements IModuleStore { }); }); - await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data, this.rootStore.user.currentUser); + const user = this.rootStore.user.currentUser ?? undefined; + + const response = await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data, user); + + return response; } catch (error) { console.error("Failed to update module in module store", error); @@ -252,6 +269,8 @@ export class ModuleStore implements IModuleStore { runInAction(() => { this.error = error; }); + + throw error; } }; @@ -334,4 +353,46 @@ export class ModuleStore implements IModuleStore { }); } }; + + updateModuleGanttStructure = ( + workspaceSlug: string, + projectId: string, + module: IModule, + payload: IBlockUpdateData + ) => { + const modulesList = this.modules[projectId]; + + try { + const newModules = modulesList?.map((p: any) => ({ + ...p, + ...(p.id === module.id + ? { + start_date: payload.start_date ? payload.start_date : p.start_date, + target_date: payload.target_date ? payload.target_date : p.target_date, + sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newModules.splice(payload.sort_order.sourceIndex, 1)[0]; + newModules.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: newModules, + }; + }); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; + + this.updateModuleDetails(workspaceSlug, module.project, module.id, newPayload); + } catch (error) { + console.error("Failed to update module gantt structure in module store", error); + } + }; }