refactor: modules list page (#2521)

* refactor: modules list page

* chore: update handle favorites logic for modules

* fix: build errors
This commit is contained in:
Aaryan Khandelwal 2023-10-23 19:17:42 +05:30 committed by GitHub
parent 07d548ea43
commit d72d3da6de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 272 additions and 357 deletions

View File

@ -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";

View File

@ -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<SetStateAction<"grid" | "gantt_chart">>;
}
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<IModulesHeader> = (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 (
<div
@ -48,7 +49,7 @@ export const ModulesHeader: FC<IModulesHeader> = (props) => {
</Link>
}
/>
<BreadcrumbItem title={`${truncateText(name ?? "Project", 32)} Modules`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Modules`} />
</Breadcrumbs>
</div>
</div>
@ -83,4 +84,4 @@ export const ModulesHeader: FC<IModulesHeader> = (props) => {
</div>
</div>
);
};
});

View File

@ -119,7 +119,7 @@ export const CycleIssueQuickActions: React.FC<Props> = (props) => {
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<div className="flex items-center gap-2">
<Trash2 className="h-3 w-3" />
Delete issue
</div>

View File

@ -119,7 +119,7 @@ export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<div className="flex items-center gap-2">
<Trash2 className="h-3 w-3" />
Delete issue
</div>

View File

@ -106,7 +106,7 @@ export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<div className="flex items-center gap-2">
<Trash2 className="h-3 w-3" />
Delete issue
</div>

View File

@ -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<IIssue>) => Promise<void>;
}
const moduleService = new ModuleService();
const issueService = new IssueService();
const issueDraftService = new IssueDraftService();
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
@ -170,35 +167,15 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = 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<IIssue>) => {
@ -267,10 +244,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
};
const updateIssue = async (payload: Partial<IIssue>) => {
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();

View File

@ -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<React.SetStateAction<boolean>>;
data?: IModule;
user: IUser | undefined;
onClose: () => void;
};
// services
const moduleService = new ModuleService();
export const DeleteModuleModal: React.FC<Props> = observer((props) => {
const { data, isOpen, onClose } = props;
export const DeleteModuleModal: React.FC<Props> = ({ 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<IModule[]>(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<Props> = ({ 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<Props> = ({ isOpen, setIsOpen, data, us
</Dialog>
</Transition.Root>
);
};
});

View File

@ -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<IModule[]>;
};
// services
const moduleService = new ModuleService();
export const ModulesListGanttChartView: FC<Props> = ({ 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<Props> = ({ modules, mutateModules })
/>
</div>
);
};
});

View File

@ -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";

View File

@ -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<IModule> = {
members_list: [],
};
const moduleService = new ModuleService();
export const CreateUpdateModuleModal: React.FC<Props> = (props) => {
export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
const { isOpen, onClose, data, workspaceSlug, projectId } = props;
const [activeProject, setActiveProject] = useState<string | null>(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> = (props) => {
});
const createModule = async (payload: Partial<IModule>) => {
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> = (props) => {
};
const updateModule = async (payload: Partial<IModule>) => {
await moduleService
.updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload, {} as IUser)
.then((res) => {
mutate<IModule[]>(
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> = (props) => {
</Dialog>
</Transition.Root>
);
};
});

View File

@ -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<Props> = observer((props) => {
const { module } = props;
export const SingleModuleCard: React.FC<Props> = ({ 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<Props> = ({ 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<IModule[]>(
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<IModule[]>(
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<Props> = ({ 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<Props> = ({ module, handleEditModule, us
return (
<>
<DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} user={user} />
{workspaceSlug && projectId && (
<CreateUpdateModuleModal
isOpen={editModuleModal}
onClose={() => setEditModuleModal(false)}
data={module}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
/>
)}
<DeleteModuleModal data={module} isOpen={moduleDeleteModal} onClose={() => setModuleDeleteModal(false)} />
<div className="flex flex-col divide-y divide-custom-border-200 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 text-xs">
<div className="p-4">
<div className="flex w-full flex-col gap-5">
@ -140,25 +116,25 @@ export const SingleModuleCard: React.FC<Props> = ({ module, handleEditModule, us
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem onClick={handleEditModule}>
<CustomMenu width="auto" verticalEllipsis placement="bottom-end">
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<LinkIcon className="h-3 w-3" strokeWidth={2} />
<span>Copy link</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setEditModuleModal(true)}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-3 w-3" strokeWidth={2} />
<span>Edit module</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeleteModule}>
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<Trash2 className="h-3 w-3" strokeWidth={2} />
<span>Delete module</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy module link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
@ -204,4 +180,4 @@ export const SingleModuleCard: React.FC<Props> = ({ module, handleEditModule, us
</div>
</>
);
};
});

View File

@ -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 (
<Loader className="grid grid-cols-3 gap-4 p-8">
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
</Loader>
);
return (
<>
{modulesList.length > 0 ? (
<>
{modulesView === "grid" && (
<div className="h-full overflow-y-auto p-8">
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
{modulesList.map((module) => (
<ModuleCardItem key={module.id} module={module} />
))}
</div>
</div>
)}
{modulesView === "gantt_chart" && <ModulesListGanttChartView />}
</>
) : (
<EmptyState
title="Manage your project with modules"
description="Modules are smaller, focused projects that help you group and organize issues."
image={emptyModule}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "New Module",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "m",
});
document.dispatchEvent(e);
},
}}
/>
)}
</>
);
});

View File

@ -228,7 +228,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
/>
<DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={moduleDetails} user={user} />
<DeleteModuleModal isOpen={moduleDeleteModal} onClose={() => setModuleDeleteModal(false)} data={moduleDetails} />
<div
className={`fixed top-[66px] ${
isOpen ? "right-0" : "-right-[24rem]"

View File

@ -205,6 +205,7 @@ export const ProjectSidebarListItem: React.FC<Props> = 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 && (
<CustomMenu.MenuItem onClick={handleDeleteProjectClick}>

View File

@ -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;

View File

@ -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<SelectModuleType>();
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 (
<AppLayout
header={<ModulesHeader name={activeProject?.name} modulesView={modulesView} setModulesView={setModulesView} />}
>
{workspaceSlug && projectId && (
<CreateUpdateModuleModal
isOpen={createUpdateModule}
onClose={() => {
setCreateUpdateModule(false);
}}
data={selectedModule}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
)}
{modules ? (
modules.length > 0 ? (
<>
{modulesView === "grid" && (
<div className="h-full overflow-y-auto p-8">
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
{modules.map((module) => (
<SingleModuleCard
key={module.id}
module={module}
handleEditModule={() => handleEditModule(module)}
user={user}
/>
))}
</div>
</div>
)}
{modulesView === "gantt_chart" && (
<ModulesListGanttChartView modules={modules} mutateModules={mutateModules} />
)}
</>
) : (
<EmptyState
title="Manage your project with modules"
description="Modules are smaller, focused projects that help you group and organize issues."
image={emptyModule}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "New Module",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "m",
});
document.dispatchEvent(e);
},
}}
/>
)
) : (
<Loader className="grid grid-cols-3 gap-4 p-8">
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
</Loader>
)}
</AppLayout>
);
};
const ProjectModules: NextPage = () => (
<AppLayout header={<ModulesListHeader />} withProjectWrapper>
<ModulesListView />
</AppLayout>
);
export default ProjectModules;

View File

@ -61,11 +61,11 @@ export class ModuleService extends APIService {
projectId: string,
moduleId: string,
data: Partial<IModule>,
user: any
): Promise<any> {
user: IUser | undefined
): Promise<IModule> {
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) => {

View File

@ -38,7 +38,7 @@ export interface IIssueDetailStore {
// creating issue
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
// updating issue
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => void;
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => Promise<IIssue>;
// deleting issue
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -260,6 +260,8 @@ export class IssueDetailStore implements IIssueDetailStore {
},
};
});
return response;
} catch (error) {
this.fetchIssueDetails(workspaceSlug, projectId, issueId);

View File

@ -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);

View File

@ -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<IModule>;
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => 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<IModule>
) => Promise<IModule>;
deleteModule: (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>;
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);
}
};
}