mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 14:27:23 +00:00
Feat: Specify organization role and projects when inviting users to org
This commit is contained in:
@@ -32,7 +32,8 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
.map(async (email) => {
|
||||
mutateAsync({
|
||||
inviteeEmails: [email],
|
||||
organizationId: String(localStorage.getItem("orgData.id"))
|
||||
organizationId: String(localStorage.getItem("orgData.id")),
|
||||
organizationRoleSlug: "member"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const roleQueryKeys = {
|
||||
["user-project-permissions", { workspaceId }] as const
|
||||
};
|
||||
|
||||
const getProjectRoles = async (projectId: string) => {
|
||||
export const getProjectRoles = async (projectId: string) => {
|
||||
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
||||
`/api/v1/workspace/${projectId}/roles`
|
||||
);
|
||||
|
||||
@@ -162,7 +162,7 @@ export const useAddUsersToOrg = () => {
|
||||
type Response = {
|
||||
data: {
|
||||
message: string;
|
||||
completeInviteLinks: {
|
||||
completeInviteLinks?: {
|
||||
email: string;
|
||||
link: string;
|
||||
}[];
|
||||
|
||||
@@ -151,7 +151,8 @@ export type DeletOrgMembershipDTO = {
|
||||
export type AddUserToOrgDTO = {
|
||||
inviteeEmails: string[];
|
||||
projectIds?: string[];
|
||||
organizationRoleSlug?: string;
|
||||
projectRoleSlug?: string;
|
||||
organizationRoleSlug: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -138,8 +138,12 @@ export const useGetUpgradeProjectStatus = ({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async () => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||
const fetchUserWorkspaces = async (includeRoles?: boolean) => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
|
||||
params: {
|
||||
includeRoles
|
||||
}
|
||||
});
|
||||
return data.workspaces;
|
||||
};
|
||||
|
||||
@@ -171,8 +175,8 @@ export const useGetWorkspaceById = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserWorkspaces = () =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
||||
export const useGetUserWorkspaces = (includeRoles?: boolean) =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
|
||||
|
||||
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TProjectRole } from "../roles/types";
|
||||
|
||||
export enum ProjectVersion {
|
||||
V1 = 1,
|
||||
V2 = 2,
|
||||
@@ -22,6 +24,8 @@ export type Workspace = {
|
||||
auditLogsRetentionDays: number;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
|
||||
roles?: TProjectRole[];
|
||||
};
|
||||
|
||||
export type WorkspaceEnv = {
|
||||
|
||||
+130
-81
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -25,18 +26,21 @@ import {
|
||||
useGetOrgRoles,
|
||||
useGetUserWorkspaces
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { OrgInviteLink } from "./OrgInviteLink";
|
||||
|
||||
const DEFAULT_ORG_MEMBER_ROLE_SLUG = "member";
|
||||
const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member";
|
||||
|
||||
const EmailSchema = z.string().email().min(1).trim().toLowerCase();
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
emails: z.string().min(1).trim().toLowerCase(),
|
||||
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
|
||||
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_MEMBER_ROLE_SLUG)
|
||||
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
||||
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
|
||||
});
|
||||
|
||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||
@@ -71,7 +75,12 @@ export const AddOrgMemberModal = ({
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
||||
|
||||
const onAddMembers = async ({ emails, organizationRoleSlug, projectIds }: TAddMemberForm) => {
|
||||
const onAddMembers = async ({
|
||||
emails,
|
||||
organizationRoleSlug,
|
||||
projectIds,
|
||||
projectRoleSlug
|
||||
}: TAddMemberForm) => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
@@ -98,7 +107,8 @@ export const AddOrgMemberModal = ({
|
||||
organizationId: currentOrg?.id,
|
||||
inviteeEmails: emails.split(",").map((email) => email.trim()),
|
||||
organizationRoleSlug,
|
||||
projectIds
|
||||
projectIds,
|
||||
projectRoleSlug
|
||||
});
|
||||
|
||||
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
||||
@@ -128,7 +138,12 @@ export const AddOrgMemberModal = ({
|
||||
};
|
||||
|
||||
const projectIds = watch("projectIds", []);
|
||||
const { data: projects } = useGetUserWorkspaces();
|
||||
const { data: projects } = useGetUserWorkspaces(true);
|
||||
|
||||
const filteredProjects = useMemo(
|
||||
() => projects?.filter((project) => project.version === ProjectVersion.V3),
|
||||
[projects]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -179,7 +194,7 @@ export const AddOrgMemberModal = ({
|
||||
>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={DEFAULT_ORG_MEMBER_ROLE_SLUG}
|
||||
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||
{...field}
|
||||
onValueChange={(val) => field.onChange(val)}
|
||||
>
|
||||
@@ -195,83 +210,117 @@ export const AddOrgMemberModal = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="-space-y-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Add user to projects"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{projects && projects.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">
|
||||
{projectIds.length === 1
|
||||
? projects.find((project) => project.id === projectIds[0])?.name
|
||||
: `${projectIds.length} projects selected`}
|
||||
<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 projects found
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80"
|
||||
>
|
||||
{projects && projects.length > 0 ? (
|
||||
projects.map((project) => {
|
||||
const isSelected = projectIds.includes(String(project.id));
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Add user to projects"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{filteredProjects && filteredProjects.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">
|
||||
{projectIds.length === 1
|
||||
? filteredProjects.find((project) => project.id === projectIds[0])
|
||||
?.name
|
||||
: `${projectIds.length} projects selected`}
|
||||
<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 projects found
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80"
|
||||
>
|
||||
{filteredProjects && filteredProjects.length > 0 ? (
|
||||
filteredProjects.map((project) => {
|
||||
const isSelected = projectIds.includes(String(project.id));
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) => projects.length > 0 && event.preventDefault()}
|
||||
onClick={() => {
|
||||
if (projectIds.includes(String(project.id))) {
|
||||
field.onChange(
|
||||
projectIds.filter(
|
||||
(projectId: string) => projectId !== String(project.id)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...projectIds, String(project.id)]);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
filteredProjects.length > 0 && event.preventDefault()
|
||||
}
|
||||
}}
|
||||
key={`project-id-${project.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"
|
||||
>
|
||||
{project.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<p className="ml-0.5 text-xs text-mineshaft-400">
|
||||
Members will automatically be assigned the <a className="font-bold">Developer</a>{" "}
|
||||
role for the projects you select.
|
||||
</p>
|
||||
onClick={() => {
|
||||
if (projectIds.includes(String(project.id))) {
|
||||
field.onChange(
|
||||
projectIds.filter(
|
||||
(projectId: string) => projectId !== String(project.id)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...projectIds, String(project.id)]);
|
||||
}
|
||||
}}
|
||||
key={`project-id-${project.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"
|
||||
>
|
||||
{project.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-fit justify-end">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectRoleSlug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="Select which role to assign to the users in the selected projects."
|
||||
label="Role"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<div>
|
||||
<Select
|
||||
isDisabled={projectIds.length === 0}
|
||||
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||
{...field}
|
||||
onValueChange={(val) => field.onChange(val)}
|
||||
>
|
||||
{Object.entries(ProjectMembershipRole).map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
([_, slug]) =>
|
||||
slug !== "custom" && (
|
||||
<SelectItem key={slug} value={slug}>
|
||||
<span className="capitalize">{slug.replace("-", " ")}</span>
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
|
||||
+2
-1
@@ -108,7 +108,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
try {
|
||||
const { data } = await addUsersMutateAsync({
|
||||
organizationId: orgId,
|
||||
inviteeEmails: [email]
|
||||
inviteeEmails: [email],
|
||||
organizationRoleSlug: "member"
|
||||
});
|
||||
|
||||
setCompleteInviteLinks(data?.completeInviteLinks || null);
|
||||
|
||||
@@ -50,7 +50,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
try {
|
||||
const { data } = await inviteUsers({
|
||||
organizationId: orgId,
|
||||
inviteeEmails: [email]
|
||||
inviteeEmails: [email],
|
||||
organizationRoleSlug: "member"
|
||||
});
|
||||
|
||||
// setCompleteInviteLink(data?.completeInviteLink || "");
|
||||
|
||||
Reference in New Issue
Block a user