From 4930d7fc025e685a7a65f6d3016bf317bd559ade Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Sep 2024 19:20:18 +0400 Subject: [PATCH] feat: user groups section --- .../UserProjectsSection/UserGroupsRow.tsx | 196 ++++++++++++++++++ .../UserProjectsSection/UserGroupsSection.tsx | 68 ++++++ .../UserProjectsSection/UserGroupsTable.tsx | 43 ++++ 3 files changed, 307 insertions(+) create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx new file mode 100644 index 000000000..3e99d8e78 --- /dev/null +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx @@ -0,0 +1,196 @@ +import { useCallback, useMemo, useState } from "react"; +import Link from "next/link"; +import { faFolder, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + EmptyState, + IconButton, + Input, + Modal, + ModalContent, + Table, + TableContainer, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useDeleteGroupFromWorkspace } from "@app/hooks/api"; +import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + group: TGroupWithProjectMemberships; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void; +}; + +const GroupProjectsModal = ({ + isOpen, + group, + groupSlug, + setIsOpen +}: { + setIsOpen: (open: boolean) => void; + isOpen: boolean; + group: TGroupWithProjectMemberships; + groupSlug: string; +}) => { + const [filteredProjectName, setFilteredProjectName] = useState(""); + + const { mutateAsync: deleteGroupFromProject } = useDeleteGroupFromWorkspace(); + + const filteredMemberships = useMemo(() => { + return group.projectMemberships.filter(({ project }) => { + return project.name.toLowerCase().includes(filteredProjectName.toLowerCase()); + }); + }, [filteredProjectName, group]); + + const removeProjectFromGroup = useCallback(async (projectSlug: string) => { + await deleteGroupFromProject({ + groupSlug, + projectSlug + }); + + createNotification({ + type: "success", + text: "Project removed from group" + }); + }, []); + + return ( + { + setIsOpen(open); + }} + > + + setFilteredProjectName(e.target.value)} + leftIcon={} + placeholder="Search projects..." + /> + + + + + + + + + {filteredMemberships?.map(({ project, roles }) => { + return ( + + + + + ); + })} + +
Project
+ +

+ {project.name} +

+ + {/* If there's only one role, display the role.name. If more, display role.name (+X more) */} + role.role.charAt(0).toUpperCase() + role.role.slice(1)) + .join(", ")} + > +

+ {roles.length === 1 + ? roles[0].role + : `${roles[0].role} (+${roles.length - 1} more)`} +

+
+
+ + {(isAllowed) => { + return ( +
+ +
+ ); + }} +
+
+ {!filteredMemberships.length && } +
+
+
+ ); +}; + +export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => { + const [isProjectsModalOpen, setIsProjectsModalOpen] = useState(false); + + return ( + <> + setIsProjectsModalOpen(true)} + > + {group.name} + + {group.projectMemberships.length} Project + {group.projectMemberships.length > 1 || group.projectMemberships.length === 0 ? "s" : ""} + + + {true && ( +
+ + { + e.stopPropagation(); + handlePopUpOpen("removeUserFromGroup", { + groupSlug: group.slug + }); + }} + > + + + +
+ )} + + + + + ); +}; diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx new file mode 100644 index 000000000..41340aec4 --- /dev/null +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx @@ -0,0 +1,68 @@ +import { useCallback } from "react"; + +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal } from "@app/components/v2"; +import { useRemoveUserFromGroup } from "@app/hooks/api"; +import { OrgUser } from "@app/hooks/api/users/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { UserGroupsTable } from "./UserGroupsTable"; + +type Props = { + orgMembership: OrgUser; +}; + +export const UserGroupsSection = ({ orgMembership }: Props) => { + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "removeUserFromGroup" + ] as const); + + const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup(); + + const handleRemoveUserFromGroup = useCallback(async (groupSlug: string) => { + try { + await removeUserFromGroup({ + slug: groupSlug, + username: orgMembership.user.username + }); + + createNotification({ + type: "success", + text: "User removed from group successfully" + }); + + handlePopUpClose("removeUserFromGroup"); + } catch (error) { + createNotification({ + type: "error", + text: "Failed to remove user from group" + }); + } + }, []); + + return ( + <> +
+
+

Groups

+
+ + +
+ + handlePopUpToggle("removeUserFromGroup", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const popupData = popUp?.removeUserFromGroup?.data as { + groupSlug: string; + }; + + return handleRemoveUserFromGroup(popupData.groupSlug); + }} + /> + + ); +}; diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx new file mode 100644 index 000000000..914df9985 --- /dev/null +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx @@ -0,0 +1,43 @@ +import { faFolder } from "@fortawesome/free-solid-svg-icons"; + +import { EmptyState, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2"; +import { OrgUser } from "@app/hooks/api/types"; +import { useListUserGroupMemberships } from "@app/hooks/api/users/queries"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { UserGroupsRow } from "./UserGroupsRow"; + +type Props = { + orgMembership: OrgUser; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void; +}; + +export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { + const { data: groups, isLoading } = useListUserGroupMemberships(orgMembership.user.username); + + return ( + + + + + + + + + + {groups?.map((group) => ( + + ))} + +
NameProjects +
+ {!isLoading && !groups?.length && ( + + )} +
+ ); +};