mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 21:40:31 +00:00
improvements: ui improvements & add users to org from project member invite modal
This commit is contained in:
+125
-85
@@ -1,12 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2";
|
||||
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddUsersToOrg,
|
||||
@@ -19,7 +19,15 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1),
|
||||
orgMemberships: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().trim(),
|
||||
value: z.string().trim(),
|
||||
isNewInvitee: z.boolean().optional()
|
||||
})
|
||||
)
|
||||
.min(1),
|
||||
projectRoleSlugs: z.array(z.object({ slug: z.string().trim(), name: z.string().trim() })).min(1)
|
||||
});
|
||||
|
||||
@@ -60,7 +68,12 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (!currentWorkspace) return;
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
const selectedMembers = orgMemberships.map((orgMembership) =>
|
||||
const existingMembers = orgMemberships.filter((membership) => !membership.isNewInvitee);
|
||||
const newInvitees = orgMemberships
|
||||
.filter((membership) => membership.isNewInvitee)
|
||||
.map((membership) => membership.value);
|
||||
|
||||
const selectedMembers = existingMembers.map((orgMembership) =>
|
||||
orgUsers?.find((orgUser) => orgUser.id === orgMembership.value)
|
||||
);
|
||||
|
||||
@@ -76,11 +89,11 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const inviteeEmails = selectedMembers
|
||||
.map((member) => member?.user.username as string)
|
||||
.filter(Boolean);
|
||||
if (inviteeEmails.length) {
|
||||
if (inviteeEmails.length || newInvitees.length) {
|
||||
await addMembersToProject({
|
||||
inviteeEmails,
|
||||
inviteeEmails: [...inviteeEmails, ...newInvitees],
|
||||
organizationId: orgId,
|
||||
organizationRoleSlug: ProjectMembershipRole.Member, // ? This doesn't apply in this case, because we know the users being added are already part of the organization
|
||||
organizationRoleSlug: ProjectMembershipRole.Member, // only applies to new invites
|
||||
projects: [
|
||||
{
|
||||
slug: currentWorkspace.slug,
|
||||
@@ -125,6 +138,8 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const selectedOrgMemberships = watch("orgMemberships");
|
||||
const selectedRoleSlugs = watch("projectRoleSlugs");
|
||||
|
||||
const { append } = useFieldArray<TAddMemberForm>({ control, name: "orgMemberships" });
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addMember?.isOpen}
|
||||
@@ -135,87 +150,112 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
title={t("section.members.add-dialog.add-member-to-project") as string}
|
||||
subTitle={t("section.members.add-dialog.user-will-email")}
|
||||
>
|
||||
{filteredOrgUsers.length ? (
|
||||
<form onSubmit={handleSubmit(onAddMembers)}>
|
||||
<div className="flex w-full flex-col items-start gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="orgMemberships"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
isError={!!errors.orgMemberships?.length}
|
||||
errorText={errors.orgMemberships?.[0]?.message}
|
||||
label="Invite users to project"
|
||||
>
|
||||
<FilterableSelect
|
||||
className="w-full"
|
||||
placeholder="Add one or more users..."
|
||||
isMulti
|
||||
name="members"
|
||||
options={filteredOrgUsers}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onAddMembers)}>
|
||||
<div className="flex w-full flex-col items-start gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="orgMemberships"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
isError={!!errors.orgMemberships?.length}
|
||||
errorText={errors.orgMemberships?.[0]?.message}
|
||||
label="Invite users to project"
|
||||
helperText="You can invite new users to your organzation by typing out their email address"
|
||||
>
|
||||
<CreatableSelect
|
||||
noOptionsMessage={() => (
|
||||
<>
|
||||
<p>
|
||||
{!filteredOrgUsers.length && (
|
||||
<p>All organization members are already assigned to this project.</p>
|
||||
)}
|
||||
</p>
|
||||
<p>Add new users to your organization by typing out their email address.</p>
|
||||
</>
|
||||
)}
|
||||
onCreateOption={(inputValue) =>
|
||||
append({ label: inputValue, value: inputValue, isNewInvitee: true })
|
||||
}
|
||||
formatCreateLabel={(inputValue) => `Invite "${inputValue}"`}
|
||||
isValidNewOption={(input) =>
|
||||
Boolean(input) &&
|
||||
z.string().email().safeParse(input).success &&
|
||||
!orgUsers
|
||||
?.flatMap(({ user }) => {
|
||||
const emails: string[] = [];
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectRoleSlugs"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
if (user.email) {
|
||||
emails.push(user.email);
|
||||
}
|
||||
|
||||
if (user.username) {
|
||||
emails.push(user.username);
|
||||
}
|
||||
|
||||
return emails;
|
||||
})
|
||||
.includes(input)
|
||||
}
|
||||
className="w-full"
|
||||
label="Select roles"
|
||||
tooltipText="Select the roles that you wish to assign to the users"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={roles}
|
||||
placeholder="Select roles..."
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={
|
||||
isSubmitting ||
|
||||
selectedOrgMemberships.length === 0 ||
|
||||
selectedRoleSlugs.length === 0
|
||||
}
|
||||
>
|
||||
Add Members
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("addMember", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>All the users in your organization are already invited.</div>
|
||||
<Link to={"/organization/access-management" as const}>
|
||||
<Button variant="outline_bg">Add users to organization</Button>
|
||||
</Link>
|
||||
placeholder="Add one or more users..."
|
||||
isMulti
|
||||
name="members"
|
||||
options={filteredOrgUsers}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectRoleSlugs"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
label="Select roles"
|
||||
tooltipText="Select the roles that you wish to assign to the users"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={roles}
|
||||
placeholder="Select roles..."
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={
|
||||
isSubmitting ||
|
||||
selectedOrgMemberships.length === 0 ||
|
||||
selectedRoleSlugs.length === 0
|
||||
}
|
||||
>
|
||||
Add Members
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("addMember", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user