Merge remote-tracking branch 'origin/main' into feat/kmip-client-management

This commit is contained in:
Sheen Capadngan
2025-02-18 02:50:12 +08:00
507 changed files with 10002 additions and 2517 deletions
@@ -1,13 +1,19 @@
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 { useOrganization, useWorkspace } from "@app/context";
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission,
useWorkspace
} from "@app/context";
import {
useAddUsersToOrg,
useGetOrgUsers,
@@ -19,7 +25,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)
});
@@ -34,6 +48,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { permission } = useOrgPermission();
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
@@ -60,7 +75,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 +96,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 +145,13 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
const selectedOrgMemberships = watch("orgMemberships");
const selectedRoleSlugs = watch("projectRoleSlugs");
const { append } = useFieldArray<TAddMemberForm>({ control, name: "orgMemberships" });
const canInviteNewMembers = permission.can(
OrgPermissionActions.Create,
OrgPermissionSubjects.Member
);
return (
<Modal
isOpen={popUp?.addMember?.isOpen}
@@ -135,19 +162,70 @@ 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"
>
<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={
canInviteNewMembers
? "You can invite new users to your organization by typing out their email address"
: undefined
}
>
{canInviteNewMembers ? (
<CreatableSelect
/* eslint-disable-next-line react/no-unstable-nested-components */
noOptionsMessage={() => (
<>
<p>
{!filteredOrgUsers.length && (
<p>All organization members are already assigned to this project.</p>
)}
</p>
<p>
Invite 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[] = [];
if (user.email) {
emails.push(user.email);
}
if (user.username) {
emails.push(user.username);
}
return emails;
})
.includes(input)
}
className="w-full"
placeholder="Add one or more users..."
isMulti
name="members"
options={filteredOrgUsers}
value={field.value}
onChange={field.onChange}
/>
) : (
<FilterableSelect
className="w-full"
placeholder="Add one or more users..."
@@ -157,65 +235,58 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
value={field.value}
onChange={field.onChange}
/>
</FormControl>
)}
/>
)}
</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>
) : (
<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>
<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>
);