feat: role list section completed with delete

This commit is contained in:
=
2024-10-21 12:38:56 +05:30
parent 558a809b4c
commit 3e8f1d8de7
30 changed files with 444 additions and 53 deletions

View File

@@ -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: {

View File

@@ -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",

View File

@@ -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: [

View File

@@ -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<TUserEncryptionKeys>(
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: [

View File

@@ -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
};
};

View File

@@ -14,6 +14,10 @@ export type TGetProjectMembershipByUsernameDTO = {
username: string;
} & TProjectPermission;
export type TGetProjectMembershipByIdDTO = {
id: string;
} & TProjectPermission;
export type TUpdateProjectMembershipDTO = {
membershipId: string;
roles: (

View File

@@ -19,14 +19,14 @@ export const useCreateProjectRole = () => {
const queryClient = useQueryClient();
return useMutation<TProjectRole, {}, TCreateProjectRoleDTO>({
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<TProjectRole, {}, TUpdateProjectRoleDTO>({
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<TProjectRole, {}, TDeleteProjectRoleDTO>({
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));
}
});
};

View File

@@ -38,7 +38,7 @@ const glob: JsInterpreter<FieldCondition<string>> = (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) =>

View File

@@ -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<Omit<TCreateProjectRoleDTO, "orgId">>;
export type TDeleteProjectRoleDTO = {
projectSlug: string;
projectId: string;
id: string;
};

View File

@@ -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;
}

View File

@@ -36,6 +36,7 @@ export {
useUpdateIdentityWorkspaceRole,
useUpdateUserWorkspaceRole,
useUpdateWsEnvironment,
useUpgradeProject
useUpgradeProject,
useGetWorkspaceUserDetails
} from "./queries";
export { workspaceKeys } from "./query-keys";

View File

@@ -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();

View File

@@ -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

View File

@@ -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 (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<MemberDetailsPage />
</>
);
}
Page.requireAuth = true;

View File

@@ -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();

View File

@@ -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 (
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white">
<div className="mb-4">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/project/${workspaceId}/members`);
}}
className="mb-4"
>
Project Access Control
</Button>
</div>
<div className="mb-4">
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between ">
<h3 className="text-lg font-semibold text-mineshaft-100">Project User Management</h3>
<div>
<Button colorSchema="danger" variant="outline_bg" size="xs">
Remove
</Button>
</div>
</div>
<div className="">
{membershipDetails && (
<p className="capitalize">
{membershipDetails.user.firstName || membershipDetails.user.lastName
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
: "-"}
</p>
)}
<div className="mt-2 text-sm text-gray-400">
Joined on{" "}
{membershipDetails?.createdAt &&
format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")}
</div>
</div>
</div>
</div>
<MemberRoleDetailsSection
membershipDetails={membershipDetails}
isMembershipDetailsLoading={isMembershipDetailsLoading}
/>
</div>
);
},
{
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Member
}
);

View File

@@ -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 (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
{userId !== membershipDetails?.user?.id && membershipDetails?.status !== "invited" && (
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
)}
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Role</Th>
<Th>Type</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isMembershipDetailsLoading && (
<TableSkeleton columns={3} innerKey="user-project-memberships" />
)}
{!isMembershipDetailsLoading &&
membershipDetails?.roles?.map((roleDetails) => {
return (
<Tr className="group h-10" key={`user-project-membership-${roleDetails?.id}`}>
<Td>
{roleDetails.role === "custom"
? roleDetails.customRoleName
: roleDetails.role}
</Td>
<Td>{!roleDetails.isTemporary ? "permanent" : "temporary"}</Td>
<Td>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Remove">
<IconButton
colorSchema="danger"
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", {
id: roleDetails?.id,
slug: roleDetails?.customRoleSlug || roleDetails?.role
});
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isMembershipDetailsLoading && !membershipDetails?.roles?.length && (
<EmptyState title="This user has not been assigned to any projects" icon={faFolder} />
)}
</TableContainer>
</div>
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
deleteKey="remove"
title={`Do you want to remove role ${(popUp?.deleteRole?.data as TProjectRole)?.slug}?`}
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
onDeleteApproved={() => handleRoleDelete()}
/>
</div>
);
};

View File

@@ -0,0 +1 @@
export { MemberRoleDetailsSection } from "./MemberRoleDetailsSection";

View File

@@ -0,0 +1 @@
export { MemberDetailsPage } from "./MemberDetailPage";

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 (
<Tr key={`membership-${membershipId}`} className="group w-full">
<Tr
key={`membership-${membershipId}`}
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
router.push(`/project/${workspaceId}/members/${membershipId}`);
}
}}
onClick={() => router.push(`/project/${workspaceId}/members/${membershipId}`)}
>
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
@@ -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 });
}}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
@@ -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 });
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>

View File

@@ -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" });