diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupProjectsModal.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupProjectsModal.tsx new file mode 100644 index 000000000..b00d44df1 --- /dev/null +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupProjectsModal.tsx @@ -0,0 +1,160 @@ +import { useCallback, useMemo, useState } from "react"; +import Link from "next/link"; +import { faFolder, faMagnifyingGlass } 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, + 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"; + +export type UserGroupsProjectsModalData = { + group: TGroupWithProjectMemberships; + groupSlug: string; + username: string; +}; + +export const UserGroupsProjectsModal = ({ + popUp, + handlePopUpToggle +}: { + popUp: UsePopUpState<["userGroupsProjects"]>; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["userGroupsProjects"]>, + state?: boolean + ) => void; +}) => { + const popupData = popUp.userGroupsProjects.data as UserGroupsProjectsModalData; + + const [filteredProjectName, setFilteredProjectName] = useState(""); + + const { mutateAsync: deleteGroupFromProject } = useDeleteGroupFromWorkspace(); + + const filteredMemberships = useMemo(() => { + if (!popupData?.group) return []; + + return popupData.group.projectMemberships.filter(({ project }) => { + return project.name.toLowerCase().includes(filteredProjectName.toLowerCase()); + }); + }, [filteredProjectName, popupData?.group?.projectMemberships]); + + const removeProjectFromGroup = useCallback( + async (projectSlug: string) => { + await deleteGroupFromProject({ + groupSlug: popupData.groupSlug, + projectSlug, + username: popupData.username + }); + + createNotification({ + type: "success", + text: "Project removed from group" + }); + + // Manually remove the project from the list, since the query invalidation won't trigger the popup data to be updated + popupData.group.projectMemberships = popupData.group.projectMemberships.filter( + (membership) => membership.project.slug !== projectSlug + ); + }, + [popupData] + ); + + return ( + { + handlePopUpToggle("userGroupsProjects", 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 && } +
+
+
+ ); +}; diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx index 3e99d8e78..d3de7919a 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx @@ -1,161 +1,36 @@ -import { useCallback, useMemo, useState } from "react"; -import Link from "next/link"; -import { faFolder, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons"; +/* eslint-disable react/jsx-no-useless-fragment */ +import { 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 { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types"; +import { OrgUser } from "@app/hooks/api/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { UserGroupsProjectsModalData } from "./UserGroupProjectsModal"; + type Props = { group: TGroupWithProjectMemberships; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void; + orgMembership: OrgUser; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeUserFromGroup", "userGroupsProjects"]>, + 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); - +export const UserGroupsRow = ({ group, orgMembership, handlePopUpOpen }: Props) => { return ( <> setIsProjectsModalOpen(true)} + onClick={() => { + handlePopUpOpen("userGroupsProjects", { + groupSlug: group.slug, + username: orgMembership.user.username, + group + } as UserGroupsProjectsModalData); + }} > {group.name} @@ -185,12 +60,6 @@ export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => { )} - ); }; diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx index 41340aec4..ace999286 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx @@ -6,6 +6,7 @@ import { useRemoveUserFromGroup } from "@app/hooks/api"; import { OrgUser } from "@app/hooks/api/users/types"; import { usePopUp } from "@app/hooks/usePopUp"; +import { UserGroupsProjectsModal } from "./UserGroupProjectsModal"; import { UserGroupsTable } from "./UserGroupsTable"; type Props = { @@ -14,7 +15,8 @@ type Props = { export const UserGroupsSection = ({ orgMembership }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "removeUserFromGroup" + "removeUserFromGroup", + "userGroupsProjects" ] as const); const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup(); @@ -63,6 +65,8 @@ export const UserGroupsSection = ({ orgMembership }: Props) => { 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 index 914df9985..7cf1ff225 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsTable.tsx @@ -9,7 +9,10 @@ import { UserGroupsRow } from "./UserGroupsRow"; type Props = { orgMembership: OrgUser; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeUserFromGroup", "userGroupsProjects"]>, + data?: {} + ) => void; }; export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { @@ -28,6 +31,7 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { {groups?.map((group) => (