From 4d711ae1493f4d0dfc9d2d22f8b2c013a59a4c93 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 26 Jul 2024 11:00:47 -0700 Subject: [PATCH] Finish project role page --- .../src/ee/routes/v1/project-role-router.ts | 4 +- .../project-role/project-role-service.ts | 9 +- frontend/src/hooks/api/roles/mutation.tsx | 32 +++++-- .../ProjectRoleListTab/ProjectRoleListTab.tsx | 21 +---- .../ProjectRoleList/ProjectRoleList.tsx | 64 ++----------- .../src/views/Project/RolePage/RolePage.tsx | 50 +++++----- .../Project/RolePage/components/RoleModal.tsx | 91 ++++++------------- .../RolePermissionRow.tsx | 13 --- .../RolePermissionsSection.tsx | 6 +- 9 files changed, 97 insertions(+), 193 deletions(-) diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index 69038a057..3ada2e50c 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -101,7 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { message: "Slug must be a valid" }), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), - permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions) + permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() }), response: { 200: z.object({ @@ -120,7 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: req.params.roleId, data: { ...req.body, - permissions: JSON.stringify(packRules(req.body.permissions)) + permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined } }); return { role }; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index ffd446fad..8c71a6e02 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -162,12 +162,19 @@ export const projectRoleServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); + if (data?.slug) { const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); if (existingRole && existingRole.id !== roleId) throw new BadRequestError({ name: "Update Role", message: "Duplicate role" }); } - const [updatedRole] = await projectRoleDAL.update({ id: roleId, projectId }, data); + const [updatedRole] = await projectRoleDAL.update( + { id: roleId, projectId }, + { + ...data, + permissions: data.permissions ? data.permissions : undefined + } + ); if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) }; }; diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 7f831cd4c..3f782171e 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -10,6 +10,7 @@ import { TDeleteOrgRoleDTO, TDeleteProjectRoleDTO, TOrgRole, + TProjectRole, TUpdateOrgRoleDTO, TUpdateProjectRoleDTO } from "./types"; @@ -17,9 +18,13 @@ import { export const useCreateProjectRole = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => - apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto), + return useMutation({ + mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => { + const { + data: { role } + } = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto); + return role; + }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); } @@ -29,9 +34,13 @@ export const useCreateProjectRole = () => { export const useUpdateProjectRole = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => - apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto), + return useMutation({ + mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => { + const { + data: { role } + } = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto); + return role; + }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); } @@ -40,10 +49,13 @@ export const useUpdateProjectRole = () => { export const useDeleteProjectRole = () => { const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) => - apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`), + return useMutation({ + mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => { + const { + data: { role } + } = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`); + return role; + }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); } diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx index 5eb443fb6..4a5fc37ac 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx @@ -2,29 +2,12 @@ import { motion } from "framer-motion"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { withProjectPermission } from "@app/hoc"; -import { usePopUp } from "@app/hooks"; import { ProjectRoleList } from "./components/ProjectRoleList"; -import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection"; export const ProjectRoleListTab = withProjectPermission( () => { - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const); - - return popUp.editRole.isOpen ? ( - - handlePopUpClose("editRole")} - /> - - ) : ( + return ( - handlePopUpOpen("editRole", slug)} /> + ); }, 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 4ea04c8f6..59a13c67f 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 @@ -25,14 +25,14 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { usePopUp } from "@app/hooks"; import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api"; import { TProjectRole } from "@app/hooks/api/roles/types"; +import { RoleModal } from "@app/views/Project/RolePage/components"; -type Props = { - onSelectRole: (slug?: string) => void; -}; - -export const ProjectRoleList = ({ onSelectRole }: Props) => { +export const ProjectRoleList = () => { const router = useRouter(); - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "role", + "deleteRole" + ] as const); const { currentWorkspace } = useWorkspace(); const projectSlug = currentWorkspace?.slug || ""; const projectId = currentWorkspace?.id || ""; @@ -66,11 +66,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { colorSchema="primary" type="submit" leftIcon={} - onClick={() => onSelectRole()} - // onClick={() => { - // TODO - // handlePopUpOpen("role"); - // }} + onClick={() => handlePopUpOpen("role")} isDisabled={!isAllowed} > Add Role @@ -120,10 +116,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { )} onClick={(e) => { e.stopPropagation(); - - // TODO: remove/replace - onSelectRole(role.slug); - // router.push(`/project/${projectId}/roles/${id}`); + router.push(`/project/${projectId}/roles/${slug}`); }} disabled={!isAllowed} > @@ -157,52 +150,13 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { - {/* -
- - {(isAllowed) => ( - onSelectRole(role.slug)} - variant="plain" - > - - - )} - - - {(isAllowed) => ( - handlePopUpOpen("deleteRole", role)} - variant="plain" - isDisabled={isNonMutatable || !isAllowed} - > - - - )} - -
- */} ); })} + { @@ -31,8 +31,7 @@ export const RolePage = withProjectPermission( const { data } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string); - // const { data } = useGetOrgRole(orgId, roleId); // TODO: get project role - // const { mutateAsync: deleteOrgRole } = useDeleteOrgRole(); + const { mutateAsync: deleteProjectRole } = useDeleteProjectRole(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", @@ -41,17 +40,19 @@ export const RolePage = withProjectPermission( const onDeleteRoleSubmit = async () => { try { - // if (!orgId || !roleId) return; - // await deleteOrgRole({ - // orgId, - // id: roleId - // }); - // createNotification({ - // text: "Successfully deleted organization role", - // type: "success" - // }); - // handlePopUpClose("deleteOrgRole"); - // router.push(`/org/${orgId}/members`); + if (!currentWorkspace?.slug || !data?.id) return; + + await deleteProjectRole({ + projectSlug: currentWorkspace.slug, + id: data.id + }); + + createNotification({ + text: "Successfully deleted project role", + type: "success" + }); + handlePopUpClose("deleteRole"); + router.push(`/project/${projectId}/members`); } catch (err) { console.error(err); const error = err as any; @@ -74,9 +75,7 @@ export const RolePage = withProjectPermission( variant="link" type="submit" leftIcon={} - onClick={() => { - router.push(`/project/${projectId}/members`); - }} + onClick={() => router.push(`/project/${projectId}/members`)} className="mb-4" > Roles @@ -102,12 +101,11 @@ export const RolePage = withProjectPermission( className={twMerge( !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => { - // TODO - // handlePopUpOpen("role", { - // roleId - // }); - }} + onClick={() => + handlePopUpOpen("role", { + roleSlug + }) + } disabled={!isAllowed} > Edit Role @@ -125,9 +123,7 @@ export const RolePage = withProjectPermission( ? "hover:!bg-red-500 hover:!text-white" : "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => { - handlePopUpOpen("deleteRole"); - }} + onClick={() => handlePopUpOpen("deleteRole")} disabled={!isAllowed} > Delete Role diff --git a/frontend/src/views/Project/RolePage/components/RoleModal.tsx b/frontend/src/views/Project/RolePage/components/RoleModal.tsx index 463f36a78..435378a02 100644 --- a/frontend/src/views/Project/RolePage/components/RoleModal.tsx +++ b/frontend/src/views/Project/RolePage/components/RoleModal.tsx @@ -1,23 +1,16 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; -// import { useRouter } from "next/router"; +import { useRouter } from "next/router"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; import { - // useOrganization, - useWorkspace -} from "@app/context"; -import { - // useCreateProjectRole, - // useUpdateProjectRole, - useGetProjectRoleBySlug - // useCreateOrgRole, - // useGetOrgRole, - // useUpdateOrgRole -} from "@app/hooks/api"; + useCreateProjectRole, + useGetProjectRoleBySlug, + useUpdateProjectRole} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z @@ -36,7 +29,7 @@ type Props = { }; export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { - // const router = useRouter(); + const router = useRouter(); const popupData = popUp?.role?.data as { roleSlug: string; @@ -47,10 +40,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const { data: role } = useGetProjectRoleBySlug(projectSlug, popupData?.roleSlug ?? ""); - console.log("Project RoleModal role: ", role); - - // const { mutateAsync: createProjectRole } = useCreateProjectRole(); - // const { mutateAsync: updateProjectRole } = useUpdateProjectRole(); + const { mutateAsync: createProjectRole } = useCreateProjectRole(); + const { mutateAsync: updateProjectRole } = useUpdateProjectRole(); const { control, @@ -83,59 +74,33 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ name, description, slug }: FormData) => { try { - console.log("onFormSubmit args: ", { - name, - description, - slug - }); - - if (!projectSlug || !role?.id) return; + if (!projectSlug) return; if (role) { // update - console.log("update"); - // const u1 = await updateProjectRole({ - // id: role.id, - // projectSlug, - // name, - // description, - // slug - // }); - // console.log("u1: ", u1); + await updateProjectRole({ + id: role.id, + projectSlug, + name, + description, + slug + }); + + handlePopUpToggle("role", false); } else { // create - console.log("create"); + const newRole = await createProjectRole({ + projectSlug, + name, + description, + slug, + permissions: [] + }); + + router.push(`/project/${currentWorkspace?.id}/roles/${newRole.slug}`); + handlePopUpToggle("role", false); } - // if (!orgId) return; - - // if (role) { - // // update - - // await updateOrgRole({ - // orgId, - // id: role.id, - // name, - // description, - // slug - // }); - - // handlePopUpToggle("role", false); - // } else { - // // create - - // const newRole = await createOrgRole({ - // orgId, - // name, - // description, - // slug, - // permissions: [] - // }); - - // handlePopUpToggle("role", false); - // router.push(`/org/${orgId}/roles/${newRole.id}`); - // } - createNotification({ text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`, type: "success" diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx index 163f3a839..a98bf0d93 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -78,22 +78,9 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal }); const selectedPermissionCategory = useMemo(() => { - // TODO: re-check const actions = Object.keys(rule || {}) as Array; switch (formName) { - case "secret-rollback": - case "workspace": { - const totalActions = WORKSPACE_PERMISSIONS.length; - const score = actions - .map((key) => (rule?.[key] ? 1 : 0)) - .reduce((a, b) => a + b, 0 as number); - if (isCustom) return Permission.Custom; - if (score === 0) return Permission.NoAccess; - if (score === totalActions) return Permission.FullAccess; - - return Permission.Custom; // TODO: update - } default: { const totalActions = GENERAL_PERMISSIONS.length; const score = actions diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index 6a03503ea..5010842ba 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -6,10 +6,11 @@ import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/compon import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { -formRolePermission2API, formSchema, + formRolePermission2API, + formSchema, rolePermission2Form, TFormSchema - } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; +} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; import { RolePermissionRow } from "./RolePermissionRow"; import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow"; @@ -89,7 +90,6 @@ type Props = { roleSlug: string; }; -// note: isEditable should also depend on if user has edit role permission export const RolePermissionsSection = ({ roleSlug }: Props) => { const { currentWorkspace } = useWorkspace(); const projectSlug = currentWorkspace?.slug || "";