diff --git a/apps/app/components/project/confirm-project-deletion.tsx b/apps/app/components/project/delete-project-modal.tsx similarity index 95% rename from apps/app/components/project/confirm-project-deletion.tsx rename to apps/app/components/project/delete-project-modal.tsx index 5c94772a5..69c4edef5 100644 --- a/apps/app/components/project/confirm-project-deletion.tsx +++ b/apps/app/components/project/delete-project-modal.tsx @@ -24,7 +24,7 @@ type TConfirmProjectDeletionProps = { data: IProject | null; }; -const ConfirmProjectDeletion: React.FC = (props) => { +export const DeleteProjectModal: React.FC = (props) => { const { isOpen, data, onClose, onSuccess } = props; const cancelButtonRef = useRef(null); @@ -122,12 +122,12 @@ const ConfirmProjectDeletion: React.FC = (props) = aria-hidden="true" /> -
+
Delete Project
-

+

Are you sure you want to delete project - {`"`} {selectedProject?.name} {`"`} ? All of the data related to the project will be permanently @@ -136,7 +136,7 @@ const ConfirmProjectDeletion: React.FC = (props) =

-

+

Enter the project name{" "} {selectedProject?.name} to continue: @@ -202,5 +202,3 @@ const ConfirmProjectDeletion: React.FC = (props) = ); }; - -export default ConfirmProjectDeletion; diff --git a/apps/app/components/project/index.ts b/apps/app/components/project/index.ts index bf18fe07d..3e00dd34e 100644 --- a/apps/app/components/project/index.ts +++ b/apps/app/components/project/index.ts @@ -1,5 +1,7 @@ -export * from "./single-project-card"; export * from "./create-project-modal"; +export * from "./delete-project-modal"; export * from "./join-project"; export * from "./sidebar-list"; export * from "./single-integration-card"; +export * from "./single-project-card"; +export * from "./single-sidebar-project"; diff --git a/apps/app/components/project/sidebar-list.tsx b/apps/app/components/project/sidebar-list.tsx index c36696460..c57254d88 100644 --- a/apps/app/components/project/sidebar-list.tsx +++ b/apps/app/components/project/sidebar-list.tsx @@ -1,53 +1,34 @@ import React, { useState, FC } from "react"; + import { useRouter } from "next/router"; -import Link from "next/link"; -import { Disclosure, Transition } from "@headlessui/react"; -import useSWR from "swr"; + +import useSWR, { mutate } from "swr"; // icons -import { ContrastIcon, LayerDiagonalIcon, PeopleGroupIcon } from "components/icons"; -import { ChevronDownIcon, PlusIcon, Cog6ToothIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "@heroicons/react/24/outline"; // hooks import useToast from "hooks/use-toast"; import useTheme from "hooks/use-theme"; // services import projectService from "services/project.service"; // components -import { CreateProjectModal } from "components/project"; +import { CreateProjectModal, DeleteProjectModal, SingleSidebarProject } from "components/project"; // ui -import { CustomMenu, Loader } from "components/ui"; +import { Loader } from "components/ui"; // helpers -import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; +// types +import { IFavoriteProject, IProject } from "types"; // fetch-keys import { FAVORITE_PROJECTS_LIST, PROJECTS_LIST } from "constants/fetch-keys"; -const navigation = (workspaceSlug: string, projectId: string) => [ - { - name: "Issues", - href: `/${workspaceSlug}/projects/${projectId}/issues`, - icon: LayerDiagonalIcon, - }, - { - name: "Cycles", - href: `/${workspaceSlug}/projects/${projectId}/cycles`, - icon: ContrastIcon, - }, - { - name: "Modules", - href: `/${workspaceSlug}/projects/${projectId}/modules`, - icon: PeopleGroupIcon, - }, - { - name: "Settings", - href: `/${workspaceSlug}/projects/${projectId}/settings`, - icon: Cog6ToothIcon, - }, -]; - export const ProjectSidebarList: FC = () => { + const [deleteProjectModal, setDeleteProjectModal] = useState(false); + const [projectToDelete, setProjectToDelete] = useState(null); + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; // states const [isCreateProjectModal, setCreateProjectModal] = useState(false); // theme @@ -66,6 +47,81 @@ export const ProjectSidebarList: FC = () => { ); const normalProjects = projects?.filter((p) => !p.is_favorite) ?? []; + const handleAddToFavorites = (project: IProject) => { + if (!workspaceSlug) return; + + projectService + .addProjectToFavorites(workspaceSlug as string, { + project: project.id, + }) + .then(() => { + mutate( + PROJECTS_LIST(workspaceSlug as string), + (prevData) => + (prevData ?? []).map((p) => ({ + ...p, + is_favorite: p.id === project.id ? true : p.is_favorite, + })), + false + ); + mutate(FAVORITE_PROJECTS_LIST(workspaceSlug as string)); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Successfully added the project to favorites.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = (project: IProject) => { + if (!workspaceSlug) return; + + projectService + .removeProjectFromFavorites(workspaceSlug as string, project.id) + .then(() => { + mutate( + PROJECTS_LIST(workspaceSlug as string), + (prevData) => + (prevData ?? []).map((p) => ({ + ...p, + is_favorite: p.id === project.id ? false : p.is_favorite, + })), + false + ); + mutate( + FAVORITE_PROJECTS_LIST(workspaceSlug as string), + (prevData) => (prevData ?? []).filter((p) => p.project !== project.id), + false + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Successfully removed the project from favorites.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); + }; + + const handleDeleteProject = (project: IProject) => { + setProjectToDelete(project); + setDeleteProjectModal(true); + }; + const handleCopyText = (projectId: string) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; @@ -81,6 +137,11 @@ export const ProjectSidebarList: FC = () => { return ( <> + setDeleteProjectModal(false)} + data={projectToDelete} + />

{favoriteProjects && favoriteProjects.length > 0 && (
@@ -89,97 +150,14 @@ export const ProjectSidebarList: FC = () => { const project = favoriteProject.project_detail; return ( - - {({ open }) => ( - <> - -
- {project.icon ? ( - - {String.fromCodePoint(parseInt(project.icon))} - - ) : ( - - {project?.name.charAt(0)} - - )} - - {!sidebarCollapse && ( -

- {truncateText(project?.name, 20)} -

- )} -
- -
- {!sidebarCollapse && ( - - handleCopyText(project.id)}> - Copy project link - - - )} - {!sidebarCollapse && ( - - - - )} -
-
- - - - {navigation(workspaceSlug as string, project?.id).map((item) => { - if (item.name === "Cycles" && !project.cycle_view) return; - if (item.name === "Modules" && !project.module_view) return; - - return ( - - -
-
- {!sidebarCollapse && item.name} -
- - ); - })} -
-
- - )} -
+ handleDeleteProject(project)} + handleCopyText={() => handleCopyText(project.id)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(project)} + /> ); })}
@@ -190,97 +168,14 @@ export const ProjectSidebarList: FC = () => { <> {normalProjects.length > 0 ? ( normalProjects.map((project) => ( - - {({ open }) => ( - <> - -
- {project.icon ? ( - - {String.fromCodePoint(parseInt(project.icon))} - - ) : ( - - {project?.name.charAt(0)} - - )} - - {!sidebarCollapse && ( -

- {truncateText(project?.name, 20)} -

- )} -
- -
- {!sidebarCollapse && ( - - handleCopyText(project.id)}> - Copy project link - - - )} - {!sidebarCollapse && ( - - - - )} -
-
- - - - {navigation(workspaceSlug as string, project?.id).map((item) => { - if (item.name === "Cycles" && !project.cycle_view) return; - if (item.name === "Modules" && !project.module_view) return; - - return ( - - -
-
- {!sidebarCollapse && item.name} -
- - ); - })} -
-
- - )} -
+ handleDeleteProject(project)} + handleCopyText={() => handleCopyText(project.id)} + handleAddToFavorites={() => handleAddToFavorites(project)} + /> )) ) : (
diff --git a/apps/app/components/project/single-project-card.tsx b/apps/app/components/project/single-project-card.tsx index c689d2acd..197c9b607 100644 --- a/apps/app/components/project/single-project-card.tsx +++ b/apps/app/components/project/single-project-card.tsx @@ -161,10 +161,12 @@ export const SingleProjectCard: React.FC = ({ Select to Join ) : ( - Member + + Member + )} {project.is_favorite && ( - + )} @@ -192,7 +194,7 @@ export const SingleProjectCard: React.FC = ({ position="bottom" theme="dark" > -
+
{renderShortNumericDateFormat(project.created_at)}
diff --git a/apps/app/components/project/single-sidebar-project.tsx b/apps/app/components/project/single-sidebar-project.tsx new file mode 100644 index 000000000..f23f51920 --- /dev/null +++ b/apps/app/components/project/single-sidebar-project.tsx @@ -0,0 +1,161 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; + +// headless ui +import { Disclosure, Transition } from "@headlessui/react"; +// ui +import { CustomMenu } from "components/ui"; +// icons +import { ChevronDownIcon, Cog6ToothIcon } from "@heroicons/react/24/outline"; +import { ContrastIcon, LayerDiagonalIcon, PeopleGroupIcon } from "components/icons"; +// helpers +import { truncateText } from "helpers/string.helper"; +// types +import { IProject } from "types"; + +type Props = { + project: IProject; + sidebarCollapse: boolean; + handleDeleteProject: () => void; + handleCopyText: () => void; + handleAddToFavorites?: () => void; + handleRemoveFromFavorites?: () => void; +}; + +const navigation = (workspaceSlug: string, projectId: string) => [ + { + name: "Issues", + href: `/${workspaceSlug}/projects/${projectId}/issues`, + icon: LayerDiagonalIcon, + }, + { + name: "Cycles", + href: `/${workspaceSlug}/projects/${projectId}/cycles`, + icon: ContrastIcon, + }, + { + name: "Modules", + href: `/${workspaceSlug}/projects/${projectId}/modules`, + icon: PeopleGroupIcon, + }, + { + name: "Settings", + href: `/${workspaceSlug}/projects/${projectId}/settings`, + icon: Cog6ToothIcon, + }, +]; + +export const SingleSidebarProject: React.FC = ({ + project, + sidebarCollapse, + handleDeleteProject, + handleCopyText, + handleAddToFavorites, + handleRemoveFromFavorites, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + return ( + + {({ open }) => ( + <> +
+ +
+ {project.icon ? ( + + {String.fromCodePoint(parseInt(project.icon))} + + ) : ( + + {project?.name.charAt(0)} + + )} + + {!sidebarCollapse && ( +

+ {truncateText(project?.name, 20)} +

+ )} +
+ {!sidebarCollapse && ( + + + + )} +
+ + {!sidebarCollapse && ( + + + Delete project + + {handleAddToFavorites && ( + + Add to favorites + + )} + {handleRemoveFromFavorites && ( + + Remove from favorites + + )} + + Copy project link + + + )} +
+ + + + {navigation(workspaceSlug as string, project?.id).map((item) => { + if (item.name === "Cycles" && !project.cycle_view) return; + if (item.name === "Modules" && !project.module_view) return; + + return ( + + +
+
+ {!sidebarCollapse && item.name} +
+ + ); + })} +
+
+ + )} +
+ ); +}; diff --git a/apps/app/components/ui/custom-menu.tsx b/apps/app/components/ui/custom-menu.tsx index a95c10ee0..630732a3e 100644 --- a/apps/app/components/ui/custom-menu.tsx +++ b/apps/app/components/ui/custom-menu.tsx @@ -120,21 +120,27 @@ const MenuItem: React.FC = ({ onClick, className = "", }) => ( - - `${className} ${ - active ? "bg-hover-gray" : "" - } cursor-pointer select-none gap-2 truncate rounded px-1 py-1.5 text-gray-500` - } - > - {({ close }) => + + {({ active, close }) => renderAs === "a" ? ( - {children} + + {children} + ) : ( - ) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index df3518b64..c90af0d23 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -15,7 +15,7 @@ import AppLayout from "layouts/app-layout"; import projectService from "services/project.service"; import workspaceService from "services/workspace.service"; // components -import ConfirmProjectDeletion from "components/project/confirm-project-deletion"; +import { DeleteProjectModal } from "components/project"; import EmojiIconPicker from "components/emoji-icon-picker"; // hooks import useToast from "hooks/use-toast"; @@ -138,7 +138,7 @@ const GeneralSettings: NextPage = (props) => { } settingsLayout > - setSelectedProject(null)} diff --git a/apps/app/pages/[workspaceSlug]/projects/index.tsx b/apps/app/pages/[workspaceSlug]/projects/index.tsx index 4569552ac..e877546c9 100644 --- a/apps/app/pages/[workspaceSlug]/projects/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/index.tsx @@ -10,8 +10,7 @@ import useWorkspaces from "hooks/use-workspaces"; import AppLayout from "layouts/app-layout"; // components import { JoinProjectModal } from "components/project/join-project-modal"; -import { SingleProjectCard } from "components/project"; -import ConfirmProjectDeletion from "components/project/confirm-project-deletion"; +import { DeleteProjectModal, SingleProjectCard } from "components/project"; // ui import { HeaderButton, EmptySpace, EmptySpaceItem, Loader } from "components/ui"; import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; @@ -71,7 +70,7 @@ const ProjectsPage: NextPage = () => { }); }} /> - setDeleteProject(null)} data={projects?.find((item) => item.id === deleteProject) ?? null} @@ -102,7 +101,7 @@ const ProjectsPage: NextPage = () => {
) : ( -
+
{projects.map((project) => (