diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index e4c7aca98..508e385df 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -34,6 +34,7 @@ import { isCustomOrgRole } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api"; import { TOrgRole } from "@app/hooks/api/roles/types"; +import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal"; export const OrgRoleTable = () => { @@ -44,6 +45,7 @@ export const OrgRoleTable = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", "deleteRole", + "duplicateRole", "upgradePlan" ] as const); @@ -192,6 +194,25 @@ export const OrgRoleTable = () => { )} + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("duplicateRole", role); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {!isDefaultOrgRole && ( { onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text={(popUp.upgradePlan?.data as { description: string })?.description} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleId={(popUp?.duplicateRole?.data as TOrgRole)?.id} + /> ); }; diff --git a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx index a3165709a..7acc468f8 100644 --- a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx @@ -19,6 +19,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { OrgAccessControlTabSections } from "@app/types/org"; import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components"; @@ -36,7 +37,8 @@ export const Page = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteOrgRole" + "deleteOrgRole", + "duplicateRole" ] as const); const onDeleteOrgRoleSubmit = async () => { @@ -106,6 +108,21 @@ export const Page = () => { )} + + {(isAllowed) => ( + { + handlePopUpOpen("duplicateRole"); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {(isAllowed) => ( { deleteKey="confirm" onDeleteApproved={() => onDeleteOrgRoleSubmit()} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleId={data?.id} + /> ); }; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx new file mode 100644 index 000000000..d5b78e0f9 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx @@ -0,0 +1,157 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useCreateOrgRole, useGetOrgRole } from "@app/hooks/api"; +import { TOrgRole } from "@app/hooks/api/roles/types"; +import { slugSchema } from "@app/lib/schemas"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + roleId?: string; +}; + +const schema = z + .object({ + name: z.string().min(1, "Name required"), + description: z.string(), + slug: slugSchema({ min: 1 }) + }) + .required(); + +export type FormData = z.infer; + +type ContentProps = { + role: TOrgRole; + onClose: () => void; +}; + +const Content = ({ role, onClose }: ContentProps) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: `${role.name} Duplicate` + }, + resolver: zodResolver(schema) + }); + + const createRole = useCreateOrgRole(); + const navigate = useNavigate(); + + const handleDuplicateRole = async (form: FormData) => { + try { + const newRole = await createRole.mutateAsync({ + orgId: role.orgId, + permissions: role.permissions, + ...form + }); + + createNotification({ + type: "success", + text: "Role duplicated successfully" + }); + + navigate({ + to: "/organization/roles/$roleId", + params: { + roleId: newRole.id + } + }); + + onClose(); + } catch { + createNotification({ + type: "error", + text: "Failed to duplicate role" + }); + } + }; + + return ( + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + Duplicate Role + + + Cancel + + + + ); +}; + +export const DuplicateOrgRoleModal = ({ isOpen, onOpenChange, roleId }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: role, isPending } = useGetOrgRole(currentOrg.id, roleId ?? ""); + + if (!roleId) return null; + + return ( + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( + + + Loading Role... + + ) : role ? ( + onOpenChange(false)} /> + ) : ( + + Error: could not find role with slug "{roleId}" + + )} + + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx index da93d3cc0..88f568c4a 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx @@ -13,7 +13,7 @@ import { slugSchema } from "@app/lib/schemas"; const schema = z .object({ - name: z.string(), + name: z.string().min(1, "Name required"), description: z.string(), slug: slugSchema({ min: 1 }) }) @@ -71,12 +71,6 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ name, description, slug }: FormData) => { try { - console.log("onFormSubmit args: ", { - name, - description, - slug - }); - if (!orgId) return; if (role) { diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 493a7f126..9f04ff722 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -25,7 +25,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { usePopUp } from "@app/hooks"; import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api"; import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types"; -import { DuplicateRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal"; +import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal"; export const ProjectRoleList = () => { @@ -202,7 +202,7 @@ export const ProjectRoleList = () => { onClose={() => handlePopUpClose("deleteRole")} onDeleteApproved={handleRoleDelete} /> - handlePopUpToggle("duplicateRole", isOpen)} roleSlug={(popUp?.duplicateRole?.data as TProjectRole)?.slug} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index d649e3ac6..3f5de1ca7 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -19,7 +19,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { usePopUp } from "@app/hooks/usePopUp"; -import { DuplicateRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal"; +import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsSection } from "./components/RoleDetailsSection"; @@ -175,7 +175,7 @@ const Page = () => { deleteKey="confirm" onDeleteApproved={() => onDeleteRoleSubmit()} /> - handlePopUpToggle("duplicateRole", isOpen)} roleSlug={roleSlug} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx similarity index 97% rename from frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal.tsx rename to frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx index cc7406a50..ffde05e74 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx @@ -51,7 +51,7 @@ const Content = ({ role, onClose }: ContentProps) => { const handleDuplicateRole = async (form: FormData) => { try { const newRole = await createRole.mutateAsync({ - projectId: role.projectId, + projectId: currentWorkspace.id, permissions: role.permissions, ...form }); @@ -128,7 +128,7 @@ const Content = ({ role, onClose }: ContentProps) => { ); }; -export const DuplicateRoleModal = ({ isOpen, onOpenChange, roleSlug }: Props) => { +export const DuplicateProjectRoleModal = ({ isOpen, onOpenChange, roleSlug }: Props) => { const { currentWorkspace } = useWorkspace(); const { data: role, isPending } = useGetProjectRoleBySlug(currentWorkspace.id, roleSlug ?? ""); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index e97e60edf..d64a88568 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -17,7 +17,7 @@ import { slugSchema } from "@app/lib/schemas"; const schema = z .object({ - name: z.string(), + name: z.string().min(1, "Name required"), description: z.string(), slug: slugSchema({ min: 1 }) })
Loading Role...
+ Error: could not find role with slug "{roleId}" +