mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
refactor: modules list page (#2521)
* refactor: modules list page * chore: update handle favorites logic for modules * fix: build errors
This commit is contained in:
parent
07d548ea43
commit
d72d3da6de
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
68
web/components/modules/modules-list-view.tsx
Normal file
68
web/components/modules/modules-list-view.tsx
Normal 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);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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]"
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user