mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
Feat: Invite multiple users to project with multiple roles
This commit is contained in:
@@ -12,6 +12,8 @@ import { TProjectDALFactory } from "../project/project-dal";
|
|||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
|
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
||||||
@@ -24,6 +26,7 @@ type TAddMembersToProjectArg = {
|
|||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||||
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ type AddMembersToNonE2EEProjectDTO = {
|
|||||||
emails: string[];
|
emails: string[];
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
projectId: string;
|
projectId: string;
|
||||||
projectMembershipRole: ProjectMembershipRole;
|
projectMembershipRoles: string[];
|
||||||
sendEmails?: boolean;
|
sendEmails?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,11 +51,12 @@ export const addMembersToProject = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
|
projectRoleDAL,
|
||||||
smtpService
|
smtpService
|
||||||
}: TAddMembersToProjectArg) => {
|
}: TAddMembersToProjectArg) => {
|
||||||
// Can create multiple memberships for a singular project, based on user email / username
|
// Can create multiple memberships for a singular project, based on user email / username
|
||||||
const addMembersToNonE2EEProject = async (
|
const addMembersToNonE2EEProject = async (
|
||||||
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
{ emails, usernames, projectId, projectMembershipRoles, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
||||||
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
||||||
) => {
|
) => {
|
||||||
const processTransaction = async (tx: Knex) => {
|
const processTransaction = async (tx: Knex) => {
|
||||||
@@ -119,7 +123,6 @@ export const addMembersToProject = ({
|
|||||||
userPrivateKey: botPrivateKey,
|
userPrivateKey: botPrivateKey,
|
||||||
members: orgMembers.map((membership) => ({
|
members: orgMembers.map((membership) => ({
|
||||||
orgMembershipId: membership.id,
|
orgMembershipId: membership.id,
|
||||||
projectMembershipRole,
|
|
||||||
userPublicKey: membership.user.publicKey
|
userPublicKey: membership.user.publicKey
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
@@ -136,10 +139,37 @@ export const addMembersToProject = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await projectUserMembershipRoleDAL.insertMany(
|
|
||||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
const defaultRoles = getPredefinedRoles(projectId).map((i) => i.slug) as string[];
|
||||||
tx
|
for await (const projectMembershipRole of projectMembershipRoles) {
|
||||||
);
|
// Custom role
|
||||||
|
if (!defaultRoles.includes(projectMembershipRole)) {
|
||||||
|
const role = await projectRoleDAL.findOne({
|
||||||
|
projectId,
|
||||||
|
slug: projectMembershipRole
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new BadRequestError({ message: `Custom role "${projectMembershipRole}" not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectUserMembershipRoleDAL.insertMany(
|
||||||
|
projectMemberships.map(({ id }) => ({
|
||||||
|
projectMembershipId: id,
|
||||||
|
role: ProjectMembershipRole.Custom,
|
||||||
|
customRoleId: role.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Predefined role
|
||||||
|
else {
|
||||||
|
await projectUserMembershipRoleDAL.insertMany(
|
||||||
|
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
members.push(...projectMemberships);
|
members.push(...projectMemberships);
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ type TProjectMembershipServiceFactoryDep = {
|
|||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "findOne">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ export type TAddUsersToWorkspaceNonE2EEDTO = {
|
|||||||
sendEmails?: boolean;
|
sendEmails?: boolean;
|
||||||
emails: string[];
|
emails: string[];
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
|
roleSlugs?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ export const useAddUserToWsNonE2EE = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
|
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
|
||||||
mutationFn: async ({ projectId, usernames }) => {
|
mutationFn: async ({ projectId, usernames, roleSlugs }) => {
|
||||||
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
|
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
|
||||||
usernames
|
usernames,
|
||||||
|
roleSlugs
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export type AddUserToWsDTOE2EE = {
|
|||||||
export type AddUserToWsDTONonE2EE = {
|
export type AddUserToWsDTONonE2EE = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
|
roleSlugs?: string[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+209
-38
@@ -2,24 +2,38 @@ import { useMemo } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalContent
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddUserToWsE2EE,
|
|
||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
|
useGetProjectRoles,
|
||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useGetWorkspaceUsers
|
useGetWorkspaceUsers
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const addMemberFormSchema = z.object({
|
const addMemberFormSchema = z.object({
|
||||||
orgMembershipId: z.string().trim()
|
orgMembershipIds: z.array(z.string().trim()).min(1),
|
||||||
|
projectRoleSlugs: z.array(z.string().trim().min(1)).min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||||
@@ -41,17 +55,22 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||||
|
|
||||||
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
|
|
||||||
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
||||||
|
|
||||||
|
const { data: roles } = useGetProjectRoles(currentWorkspace?.slug || "");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
|
watch,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
} = useForm<TAddMemberForm>({
|
||||||
|
resolver: zodResolver(addMemberFormSchema),
|
||||||
|
defaultValues: { orgMembershipIds: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
|
||||||
|
});
|
||||||
|
|
||||||
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
|
const onAddMember = async ({ orgMembershipIds, projectRoleSlugs }: TAddMemberForm) => {
|
||||||
if (!currentWorkspace) return;
|
if (!currentWorkspace) return;
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
// TODO(akhilmhdh): Move to memory storage
|
// TODO(akhilmhdh): Move to memory storage
|
||||||
@@ -62,22 +81,23 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
|
const selectedMembers = orgMembershipIds.map((orgMembershipId) =>
|
||||||
if (!orgUser) return;
|
orgUsers?.find((orgUser) => orgUser.id === orgMembershipId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedMembers) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: update
|
|
||||||
if (currentWorkspace.version === ProjectVersion.V1) {
|
if (currentWorkspace.version === ProjectVersion.V1) {
|
||||||
await addUserToWorkspace({
|
createNotification({
|
||||||
workspaceId,
|
type: "error",
|
||||||
userPrivateKey,
|
text: "Please upgrade your project to invite new members to the project."
|
||||||
decryptKey: wsKey,
|
|
||||||
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addUserToWorkspaceNonE2EE({
|
await addUserToWorkspaceNonE2EE({
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
usernames: [orgUser.user.username],
|
usernames: [...selectedMembers.map((member) => member?.user.username!)],
|
||||||
|
roleSlugs: projectRoleSlugs,
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -104,6 +124,11 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
|
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
|
||||||
}, [orgUsers, members]);
|
}, [orgUsers, members]);
|
||||||
|
|
||||||
|
const selectedOrgMembershipIds = watch("orgMembershipIds");
|
||||||
|
const selectedRoleSlugs = watch("projectRoleSlugs");
|
||||||
|
|
||||||
|
console.log(selectedOrgMembershipIds);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.addMember?.isOpen}
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
@@ -115,35 +140,181 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
>
|
>
|
||||||
{filteredOrgUsers.length ? (
|
{filteredOrgUsers.length ? (
|
||||||
<form onSubmit={handleSubmit(onAddMember)}>
|
<form onSubmit={handleSubmit(onAddMember)}>
|
||||||
<Controller
|
<div className="flex w-full items-center gap-2">
|
||||||
control={control}
|
<div className="w-full">
|
||||||
defaultValue={filteredOrgUsers?.[0]?.user?.username}
|
<Controller
|
||||||
name="orgMembershipId"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="orgMembershipIds"
|
||||||
<FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
|
render={({ field }) => (
|
||||||
<Select
|
<FormControl label="Invite users to project">
|
||||||
position="popper"
|
<DropdownMenu>
|
||||||
className="w-full"
|
<DropdownMenuTrigger asChild>
|
||||||
defaultValue={filteredOrgUsers?.[0]?.user?.username}
|
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
|
||||||
value={field.value}
|
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
onValueChange={field.onChange}
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
>
|
{selectedOrgMembershipIds.length === 1
|
||||||
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
|
? filteredOrgUsers.find(
|
||||||
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
|
(orgUser) => orgUser.id === selectedOrgMembershipIds[0]
|
||||||
{u?.username}
|
)?.user.username
|
||||||
</SelectItem>
|
: selectedOrgMembershipIds.length === 0
|
||||||
))}
|
? "No users selected"
|
||||||
</Select>
|
: `${selectedOrgMembershipIds.length} users selected`}
|
||||||
</FormControl>
|
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
|
||||||
)}
|
</div>
|
||||||
/>
|
) : (
|
||||||
|
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
No users found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="thin-scrollbar z-[100] max-h-80"
|
||||||
|
>
|
||||||
|
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
|
||||||
|
filteredOrgUsers.map((member) => {
|
||||||
|
const isSelected = selectedOrgMembershipIds.includes(member.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(event) =>
|
||||||
|
filteredOrgUsers.length > 1 && event.preventDefault()
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedOrgMembershipIds.includes(String(member.id))) {
|
||||||
|
field.onChange(
|
||||||
|
selectedOrgMembershipIds.filter(
|
||||||
|
(membershipId: string) =>
|
||||||
|
membershipId !== String(member.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
field.onChange([
|
||||||
|
...selectedOrgMembershipIds,
|
||||||
|
String(member.id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={`membership-id-${member.id}`}
|
||||||
|
icon={
|
||||||
|
isSelected ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheckCircle}
|
||||||
|
className="pr-0.5 text-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pl-[1.01rem]" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="left"
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{member.user.username}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-fit justify-end">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="projectRoleSlugs"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Select roles"
|
||||||
|
tooltipText="Select the roles that you wish to assign to the users"
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
{roles && roles.length > 0 ? (
|
||||||
|
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{selectedRoleSlugs.length === 1
|
||||||
|
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
|
||||||
|
: selectedRoleSlugs.length === 0
|
||||||
|
? "Select at least one role"
|
||||||
|
: `${selectedRoleSlugs.length} roles selected`}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronDown}
|
||||||
|
className={twMerge("ml-2 text-xs")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
No roles found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="thin-scrollbar z-[100] max-h-80"
|
||||||
|
>
|
||||||
|
{roles && roles.length > 0 ? (
|
||||||
|
roles.map((role) => {
|
||||||
|
const isSelected = selectedRoleSlugs.includes(role.slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(event) => roles.length > 1 && event.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedRoleSlugs.includes(String(role.slug))) {
|
||||||
|
field.onChange(
|
||||||
|
selectedRoleSlugs.filter(
|
||||||
|
(roleSlug: string) => roleSlug !== String(role.slug)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
field.onChange([...selectedRoleSlugs, role.slug]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={`role-slug-${role.slug}`}
|
||||||
|
icon={
|
||||||
|
isSelected ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheckCircle}
|
||||||
|
className="pr-0.5 text-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pl-[1.01rem]" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="left"
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center">
|
<div className="mt-8 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={
|
||||||
|
isSubmitting ||
|
||||||
|
selectedOrgMembershipIds.length === 0 ||
|
||||||
|
selectedRoleSlugs.length === 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Add Member
|
Add Member
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user