diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 97ff2ffb9..eeee7aaac 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -475,6 +475,7 @@ export const PROJECT_USERS = { }, GET_USER_MEMBERSHIP: { workspaceId: "The ID of the project to get memberships from.", + membershipId: "The ID of the user's project membership.", username: "The username to get project membership of. Email is the default username." }, UPDATE_USER_MEMBERSHIP: { diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index 4f92783c5..ee763e89f 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -56,7 +56,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }) ) }) - .omit({ createdAt: true, updatedAt: true }) + .omit({ updatedAt: true }) .array() }) } @@ -74,6 +74,64 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } }); + server.route({ + method: "GET", + url: "/:workspaceId/memberships/:membershipId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Return project user membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId), + membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId) + }), + response: { + 200: z.object({ + membership: ProjectMembershipsSchema.extend({ + user: UsersSchema.pick({ + email: true, + firstName: true, + lastName: true, + id: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ) + }).omit({ updatedAt: true }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const membership = await server.services.projectMembership.getProjectMembershipById({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + id: req.params.membershipId + }); + return { membership }; + } + }); + server.route({ method: "POST", url: "/:workspaceId/memberships/details", diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index dbe43c30a..a776f7245 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -158,6 +158,7 @@ export const groupProjectDALFactory = (db: TDbClient) => { ) .select( db.ref("id").withSchema(TableName.UserGroupMembership), + db.ref("createdAt").withSchema(TableName.UserGroupMembership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), @@ -181,7 +182,18 @@ export const groupProjectDALFactory = (db: TDbClient) => { const members = sqlNestRelationships({ data: docs, - parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({ + parentMapper: ({ + email, + firstName, + username, + lastName, + publicKey, + isGhost, + id, + userId, + projectName, + createdAt + }) => ({ isGroupMember: true, id, userId, @@ -190,7 +202,8 @@ export const groupProjectDALFactory = (db: TDbClient) => { id: projectId, name: projectName }, - user: { email, username, firstName, lastName, id: userId, publicKey, isGhost } + user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, + createdAt }), key: "id", childrenMapper: [ diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index 19503ba3b..bfd0c6f85 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -11,7 +11,10 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const projectMemberOrm = ormify(db, TableName.ProjectMembership); // special query - const findAllProjectMembers = async (projectId: string, filter: { usernames?: string[]; username?: string } = {}) => { + const findAllProjectMembers = async ( + projectId: string, + filter: { usernames?: string[]; username?: string; id?: string } = {} + ) => { try { const docs = await db .replicaNode()(TableName.ProjectMembership) @@ -25,6 +28,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => { if (filter.username) { void qb.where("username", filter.username); } + if (filter.id) { + void qb.where(`${TableName.ProjectMembership}.id`, filter.id); + } }) .join( TableName.UserEncryptionKey, @@ -43,6 +49,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => { ) .select( db.ref("id").withSchema(TableName.ProjectMembership), + db.ref("createdAt").withSchema(TableName.ProjectMembership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), @@ -66,7 +73,18 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const members = sqlNestRelationships({ data: docs, - parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({ + parentMapper: ({ + email, + firstName, + username, + lastName, + publicKey, + isGhost, + id, + userId, + projectName, + createdAt + }) => ({ id, userId, projectId, @@ -74,7 +92,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => { project: { id: projectId, name: projectName - } + }, + createdAt }), key: "id", childrenMapper: [ diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 71b01d615..9c43e9bd9 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -27,6 +27,7 @@ import { TAddUsersToWorkspaceDTO, TDeleteProjectMembershipOldDTO, TDeleteProjectMembershipsDTO, + TGetProjectMembershipByIdDTO, TGetProjectMembershipByUsernameDTO, TGetProjectMembershipDTO, TLeaveProjectDTO, @@ -133,6 +134,28 @@ export const projectMembershipServiceFactory = ({ return membership; }; + const getProjectMembershipById = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId, + id + }: TGetProjectMembershipByIdDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + + const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id }); + if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` }); + return membership; + }; + const addUsersToProject = async ({ projectId, actorId, @@ -487,6 +510,7 @@ export const projectMembershipServiceFactory = ({ deleteProjectMemberships, deleteProjectMembership, // TODO: Remove this addUsersToProject, - leaveProject + leaveProject, + getProjectMembershipById }; }; diff --git a/backend/src/services/project-membership/project-membership-types.ts b/backend/src/services/project-membership/project-membership-types.ts index c50ee4c1b..68819f5ae 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -14,6 +14,10 @@ export type TGetProjectMembershipByUsernameDTO = { username: string; } & TProjectPermission; +export type TGetProjectMembershipByIdDTO = { + id: string; +} & TProjectPermission; + export type TUpdateProjectMembershipDTO = { membershipId: string; roles: ( diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 1c6b85b66..78ec67e63 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -19,14 +19,14 @@ export const useCreateProjectRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => { + mutationFn: async ({ projectId, ...dto }: TCreateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.post(`/api/v2/workspace/${projectSlug}/roles`, dto); + } = await apiRequest.post(`/api/v2/workspace/${projectId}/roles`, dto); return role; }, - onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId)); } }); }; @@ -35,14 +35,14 @@ export const useUpdateProjectRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => { + mutationFn: async ({ id, projectId, ...dto }: TUpdateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/roles/${id}`, dto); + } = await apiRequest.patch(`/api/v2/workspace/${projectId}/roles/${id}`, dto); return role; }, - onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId)); } }); }; @@ -50,14 +50,14 @@ export const useUpdateProjectRole = () => { export const useDeleteProjectRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => { + mutationFn: async ({ projectId, id }: TDeleteProjectRoleDTO) => { const { data: { role } - } = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/roles/${id}`); + } = await apiRequest.delete(`/api/v2/workspace/${projectId}/roles/${id}`); return role; }, - onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId)); } }); }; diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index 52bea6e58..ebd6266a4 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -38,7 +38,7 @@ const glob: JsInterpreter> = (node, object, context) => { const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); export const roleQueryKeys = { - getProjectRoles: (projectSlug: string) => ["roles", { projectSlug }] as const, + getProjectRoles: (projectId: string) => ["roles", { projectSlug: projectId }] as const, getProjectRoleBySlug: (projectSlug: string, roleSlug: string) => ["roles", { projectSlug, roleSlug }] as const, getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const, @@ -56,11 +56,11 @@ export const getProjectRoles = async (projectId: string) => { return data.roles; }; -export const useGetProjectRoles = (projectSlug: string) => +export const useGetProjectRoles = (projectId: string) => useQuery({ - queryKey: roleQueryKeys.getProjectRoles(projectSlug), - queryFn: () => getProjectRoles(projectSlug), - enabled: Boolean(projectSlug) + queryKey: roleQueryKeys.getProjectRoles(projectId), + queryFn: () => getProjectRoles(projectId), + enabled: Boolean(projectId) }); export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) => diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index 50cdf5a22..0a48c9f97 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -72,7 +72,7 @@ export type TDeleteOrgRoleDTO = { }; export type TCreateProjectRoleDTO = { - projectSlug: string; + projectId: string; name: string; description?: string; slug: string; @@ -80,11 +80,11 @@ export type TCreateProjectRoleDTO = { }; export type TUpdateProjectRoleDTO = { - projectSlug: string; + projectId: string; id: string; } & Partial>; export type TDeleteProjectRoleDTO = { - projectSlug: string; + projectId: string; id: string; }; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 84566efcb..39a0c7c5c 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -1,4 +1,5 @@ import { UserWsKeyPair } from "../keys/types"; +import { ProjectUserMembershipTemporaryMode } from "../workspace/types"; export enum AuthMethod { EMAIL = "email", @@ -85,6 +86,7 @@ export type TWorkspaceUser = { id: string; publicKey: string; }; + createdAt: string; projectId: string; isGroupMember: boolean; project: { @@ -114,7 +116,7 @@ export type TWorkspaceUser = { customRoleSlug: string; isTemporary: true; temporaryRange: string; - temporaryMode: string; + temporaryMode: ProjectUserMembershipTemporaryMode; temporaryAccessEndTime: string; temporaryAccessStartTime: string; } diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index 454560e88..4fdf1d131 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -36,6 +36,7 @@ export { useUpdateIdentityWorkspaceRole, useUpdateUserWorkspaceRole, useUpdateWsEnvironment, - useUpgradeProject + useUpgradeProject, + useGetWorkspaceUserDetails } from "./queries"; export { workspaceKeys } from "./query-keys"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index ecefe7513..af6524fe0 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -365,6 +365,21 @@ export const useGetWorkspaceUsers = (workspaceId: string, includeGroupMembers?: }); }; +export const useGetWorkspaceUserDetails = (workspaceId: string, membershipId: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspaceUserDetails(workspaceId, membershipId), + queryFn: async () => { + const { + data: { membership } + } = await apiRequest.get<{ membership: TWorkspaceUser }>( + `/api/v1/workspace/${workspaceId}/memberships/${membershipId}` + ); + return membership; + }, + enabled: Boolean(workspaceId) && Boolean(membershipId) + }); +}; + export const useDeleteUserFromWorkspace = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index 388645fe5..8de881cff 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -15,6 +15,8 @@ export const workspaceKeys = { getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }, "workspace-audit-logs"] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const, + getWorkspaceUserDetails: (workspaceId: string, membershipId: string) => + [{ workspaceId, membershipId }, "workspace-user-details"] as const, getWorkspaceIdentityMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-identity-memberships"] as const, // allows invalidation using above key without knowing params diff --git a/frontend/src/pages/project/[id]/members/[membershipId]/index.tsx b/frontend/src/pages/project/[id]/members/[membershipId]/index.tsx new file mode 100644 index 000000000..5dfbe64bc --- /dev/null +++ b/frontend/src/pages/project/[id]/members/[membershipId]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; +import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage"; + +export default function Page() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +Page.requireAuth = true; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx index 58697afcc..23f7cc940 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; -import { useOrganization,useWorkspace } from "@app/context"; +import { useOrganization, useWorkspace } from "@app/context"; import { useAddIdentityToWorkspace, useGetIdentityProjectMemberships, @@ -50,7 +50,7 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle const projectId = watch("projectId"); const { data: projectMemberships } = useGetIdentityProjectMemberships(identityId); const { data: project } = useGetWorkspaceById(projectId); - const { data: roles } = useGetProjectRoles(project?.slug ?? ""); + const { data: roles } = useGetProjectRoles(project?.id ?? ""); const filteredWorkspaces = useMemo(() => { const wsWorkspaceIds = new Map(); diff --git a/frontend/src/views/Project/MemberDetailsPage/MemberDetailPage.tsx b/frontend/src/views/Project/MemberDetailsPage/MemberDetailPage.tsx new file mode 100644 index 000000000..509b34e0f --- /dev/null +++ b/frontend/src/views/Project/MemberDetailsPage/MemberDetailPage.tsx @@ -0,0 +1,74 @@ +import { Button } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { withProjectPermission } from "@app/hoc"; +import { useGetWorkspaceUserDetails } from "@app/hooks/api"; +import { faChevronLeft, faUser } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; +import { useRouter } from "next/router"; +import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection"; + +export const MemberDetailsPage = withProjectPermission( + () => { + const router = useRouter(); + const { currentWorkspace } = useWorkspace(); + + const workspaceId = currentWorkspace?.id || ""; + const membershipId = router.query.membershipId as string; + + const { data: membershipDetails, isLoading: isMembershipDetailsLoading } = + useGetWorkspaceUserDetails(workspaceId, membershipId); + + return ( +
+
+ +
+
+
+
+

Project User Management

+
+ +
+
+
+ {membershipDetails && ( +

+ {membershipDetails.user.firstName || membershipDetails.user.lastName + ? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}` + : "-"} +

+ )} +
+ Joined on{" "} + {membershipDetails?.createdAt && + format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")} +
+
+
+
+ +
+ ); + }, + { + action: ProjectPermissionActions.Read, + subject: ProjectPermissionSub.Member + } +); diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx new file mode 100644 index 000000000..d4f488dcf --- /dev/null +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx @@ -0,0 +1,144 @@ +import { createNotification } from "@app/components/notifications"; +import { + DeleteActionModal, + IconButton, + TableContainer, + Td, + Table, + Tr, + Th, + THead, + TableSkeleton, + EmptyState, + TBody, + Tooltip +} from "@app/components/v2"; +import { useUser, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + useDeleteProjectRole, + useGetWorkspaceUserDetails, + useUpdateUserWorkspaceRole +} from "@app/hooks/api"; +import { TProjectRole } from "@app/hooks/api/roles/types"; +import { TWorkspaceUser } from "@app/hooks/api/types"; +import { faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useRouter } from "next/router"; + +type Props = { + membershipDetails: TWorkspaceUser; + isMembershipDetailsLoading?: boolean; +}; + +export const MemberRoleDetailsSection = ({ + membershipDetails, + isMembershipDetailsLoading +}: Props) => { + const router = useRouter(); + const { user } = useUser(); + const { currentWorkspace } = useWorkspace(); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "deleteRole" + ] as const); + const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole(); + + const userId = user?.id; + + const handleRoleDelete = async () => { + const { id } = popUp?.deleteRole?.data as TProjectRole; + try { + const updatedRole = membershipDetails?.roles?.filter((el) => el.id !== id); + await updateUserWorkspaceRole({ + workspaceId: currentWorkspace?.id || "", + roles: updatedRole, + membershipId: membershipDetails.id + }); + createNotification({ type: "success", text: "Successfully removed the role" }); + handlePopUpClose("deleteRole"); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to delete role" }); + } + }; + + return ( +
+
+

Project Roles

+ {userId !== membershipDetails?.user?.id && membershipDetails?.status !== "invited" && ( + {}} + > + + + )} +
+
+ + + + + + + + + + {isMembershipDetailsLoading && ( + + )} + {!isMembershipDetailsLoading && + membershipDetails?.roles?.map((roleDetails) => { + return ( + + + + + + ); + })} + +
RoleType +
+ {roleDetails.role === "custom" + ? roleDetails.customRoleName + : roleDetails.role} + {!roleDetails.isTemporary ? "permanent" : "temporary"} +
+ + { + e.stopPropagation(); + handlePopUpOpen("deleteRole", { + id: roleDetails?.id, + slug: roleDetails?.customRoleSlug || roleDetails?.role + }); + }} + > + + + +
+
+ {!isMembershipDetailsLoading && !membershipDetails?.roles?.length && ( + + )} +
+
+ handlePopUpToggle("deleteRole", isOpen)} + onDeleteApproved={() => handleRoleDelete()} + /> +
+ ); +}; diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleModify.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/index.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/index.tsx new file mode 100644 index 000000000..cdeb3a958 --- /dev/null +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/index.tsx @@ -0,0 +1 @@ +export { MemberRoleDetailsSection } from "./MemberRoleDetailsSection"; diff --git a/frontend/src/views/Project/MemberDetailsPage/index.tsx b/frontend/src/views/Project/MemberDetailsPage/index.tsx new file mode 100644 index 000000000..cf2ac7fe6 --- /dev/null +++ b/frontend/src/views/Project/MemberDetailsPage/index.tsx @@ -0,0 +1 @@ +export { MemberDetailsPage } from "./MemberDetailPage"; diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx index b66424509..3ece05497 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx @@ -32,12 +32,11 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentWorkspace } = useWorkspace(); const orgId = currentOrg?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; const { data: groups } = useGetOrganizationGroups(orgId); const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.id || ""); - const { data: roles } = useGetProjectRoles(projectSlug); + const { data: roles } = useGetProjectRoles(currentWorkspace?.id || ""); const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace(); diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index 5563624ce..2580cfb6b 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -216,9 +216,9 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember resolver: zodResolver(formSchema) }); - const projectSlug = currentWorkspace?.slug || ""; - - const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug); + const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles( + currentWorkspace?.id ?? "" + ); const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole(); diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx index 1728d4db1..811958d37 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx @@ -42,7 +42,6 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const organizationId = currentOrg?.id || ""; const workspaceId = currentWorkspace?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; const { data: identityMembershipOrgsData } = useGetIdentityMembershipOrgs({ organizationId, @@ -59,7 +58,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { data: roles, isLoading: isRolesLoading, isFetched: isRolesFetched - } = useGetProjectRoles(projectSlug); + } = useGetProjectRoles(workspaceId); const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace(); diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx index 05aeb1fb0..2ef3f8bf3 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx @@ -65,8 +65,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; - const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug); + const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const { permission } = useProjectPermission(); const isMemberEditDisabled = permission.cannot( ProjectPermissionActions.Edit, diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx index 397ea0887..d1356324b 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx @@ -54,7 +54,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: orgUsers } = useGetOrgUsers(orgId); - const { data: roles } = useGetProjectRoles(currentWorkspace?.slug || ""); + const { data: roles } = useGetProjectRoles(currentWorkspace?.id || ""); const { control, diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx index 5cad54801..f852331dc 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx @@ -65,8 +65,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props) const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; - const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug); + const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const { permission } = useProjectPermission(); const isMemberEditDisabled = permission.cannot( ProjectPermissionActions.Edit, diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx index bffd10a61..ad4fe490c 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx @@ -7,6 +7,7 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useRouter } from "next/router"; import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; @@ -65,6 +66,7 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop const { currentWorkspace } = useWorkspace(); const { user } = useUser(); + const router = useRouter(); const userId = user?.id || ""; const workspaceId = currentWorkspace?.id || ""; @@ -111,7 +113,18 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop const email = u?.email || inviteEmail; return ( - + { + if (evt.key === "Enter") { + router.push(`/project/${workspaceId}/members/${membershipId}`); + } + }} + onClick={() => router.push(`/project/${workspaceId}/members/${membershipId}`)} + > {name} {email} @@ -202,9 +215,11 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop size="sm" variant="plain" ariaLabel="update-role" - onClick={() => - handlePopUpOpen("updateRole", { ...projectMember, index }) - } + onClick={(evt) => { + evt.preventDefault(); + evt.stopPropagation(); + handlePopUpOpen("updateRole", { ...projectMember, index }); + }} > @@ -226,9 +241,11 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop ariaLabel="update" className="ml-4" isDisabled={userId === u?.id || !isAllowed} - onClick={() => - handlePopUpOpen("removeMember", { username: u.username }) - } + onClick={(evt) => { + evt.preventDefault(); + evt.stopPropagation(); + handlePopUpOpen("removeMember", { username: u.username }); + }} > diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 658a75b57..3394cd619 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -34,10 +34,9 @@ export const ProjectRoleList = () => { "deleteRole" ] as const); const { currentWorkspace } = useWorkspace(); - const projectSlug = currentWorkspace?.slug || ""; const projectId = currentWorkspace?.id || ""; - const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug); + const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectId); const { mutateAsync: deleteRole } = useDeleteProjectRole(); @@ -45,7 +44,7 @@ export const ProjectRoleList = () => { const { id } = popUp?.deleteRole?.data as TProjectRole; try { await deleteRole({ - projectSlug, + projectId, id }); createNotification({ type: "success", text: "Successfully removed the role" });