mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
@@ -458,7 +458,6 @@ export const orgServiceFactory = ({
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
|
||||
@@ -583,6 +582,8 @@ export const orgServiceFactory = ({
|
||||
|
||||
// if there exist no org membership we set is as given by the request
|
||||
if (!inviteeMembership) {
|
||||
// as its used by project invite also
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
let roleId;
|
||||
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
|
||||
if (isCustomOrgRole) {
|
||||
|
||||
83
frontend/src/components/v2/ComboBox/ComboBox.tsx
Normal file
83
frontend/src/components/v2/ComboBox/ComboBox.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useState } from "react";
|
||||
import { faCaretDown, faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import { ByComparator } from "@headlessui/react/dist/types";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type ComboBoxProps<T extends object> = {
|
||||
value?: T;
|
||||
className?: string;
|
||||
items: {
|
||||
value: T;
|
||||
key: string;
|
||||
label: string;
|
||||
}[];
|
||||
by: ByComparator<T>;
|
||||
defaultValue?: T;
|
||||
displayValue: (value: T) => string;
|
||||
onSelectChange: (value: T) => void;
|
||||
onFilter: (value: { value: T }, filterQuery: string) => boolean;
|
||||
};
|
||||
|
||||
// TODO(akhilmhdh): This is a very temporary one due to limitation of present situation
|
||||
// don't mind the api for now will be switched to aria later
|
||||
export const ComboBox = <T extends object>({
|
||||
onSelectChange,
|
||||
onFilter,
|
||||
displayValue,
|
||||
by,
|
||||
items,
|
||||
...props
|
||||
}: ComboBoxProps<T>) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const filteredResult =
|
||||
query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)).slice(0, 20);
|
||||
|
||||
return (
|
||||
<Combobox by={by} {...props} onChange={onSelectChange}>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
displayValue={displayValue}
|
||||
className=" inline-flex w-full items-center justify-between rounded-md bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200"
|
||||
/>
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<FontAwesomeIcon icon={faCaretDown} size="sm" aria-hidden="true" />
|
||||
</Combobox.Button>
|
||||
<Transition
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setQuery("")}
|
||||
>
|
||||
<Combobox.Options className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-mineshaft-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
|
||||
{filteredResult.map(({ value, key, label }) => (
|
||||
<Combobox.Option
|
||||
key={key}
|
||||
value={value}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 transition-all hover:bg-mineshaft-500 ${
|
||||
active ? "text-primary" : "text-white"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{label}
|
||||
{selected ? (
|
||||
<div className={twMerge("absolute top-2 left-3 text-primary")}>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
1
frontend/src/components/v2/ComboBox/index.tsx
Normal file
1
frontend/src/components/v2/ComboBox/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { ComboBox } from "./ComboBox";
|
||||
@@ -11,13 +11,24 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||
title?: ReactNode;
|
||||
subTitle?: ReactNode;
|
||||
footerContent?: ReactNode;
|
||||
bodyClassName?: string;
|
||||
onClose?: () => void;
|
||||
overlayClassName?: string;
|
||||
};
|
||||
|
||||
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
(
|
||||
{ children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props },
|
||||
{
|
||||
children,
|
||||
title,
|
||||
subTitle,
|
||||
className,
|
||||
overlayClassName,
|
||||
footerContent,
|
||||
bodyClassName,
|
||||
onClose,
|
||||
...props
|
||||
},
|
||||
forwardedRef
|
||||
) => (
|
||||
<DialogPrimitive.Portal>
|
||||
@@ -35,7 +46,10 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
style={{ maxHeight: "90%" }}
|
||||
>
|
||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
||||
<CardBody className="overflow-y-auto overflow-x-hidden" style={{ maxHeight: "90%" }}>
|
||||
<CardBody
|
||||
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
>
|
||||
{children}
|
||||
</CardBody>
|
||||
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { Button, FormControl, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
import { ComboBox } from "@app/components/v2/ComboBox";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddIdentityToWorkspace,
|
||||
@@ -25,8 +18,14 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
identityId: yup.string().required("Identity id is required"),
|
||||
role: yup.string()
|
||||
identity: yup.object({
|
||||
id: yup.string().required("Identity id is required"),
|
||||
name: yup.string().required("Identity name is required")
|
||||
}),
|
||||
role: yup.object({
|
||||
slug: yup.string().required("role slug is required"),
|
||||
name: yup.string().required("role name is required")
|
||||
})
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -56,7 +55,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
const identityMemberships = identityMembershipsData?.identityMemberships;
|
||||
|
||||
const { data: roles } = useGetProjectRoles(projectSlug);
|
||||
const {
|
||||
data: roles,
|
||||
isLoading: isRolesLoading,
|
||||
isFetched: isRolesFetched
|
||||
} = useGetProjectRoles(projectSlug);
|
||||
|
||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||
|
||||
@@ -74,17 +77,24 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ identityId, role }: FormData) => {
|
||||
useEffect(() => {
|
||||
if (!isRolesFetched || !roles) return;
|
||||
|
||||
setValue("role", { name: roles[0]?.name, slug: roles[0]?.slug });
|
||||
}, [isRolesFetched, roles]);
|
||||
|
||||
const onFormSubmit = async ({ identity, role }: FormData) => {
|
||||
try {
|
||||
await addIdentityToWorkspaceMutateAsync({
|
||||
workspaceId,
|
||||
identityId,
|
||||
role: role || undefined
|
||||
identityId: identity.id,
|
||||
role: role.slug || undefined
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -92,7 +102,17 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
const nextAvailableMembership = filteredIdentityMembershipOrgs.filter(
|
||||
(membership) => membership.identity.id !== identity.id
|
||||
)[0];
|
||||
|
||||
// prevents combobox from displaying previously added identity
|
||||
reset({
|
||||
identity: {
|
||||
name: nextAvailableMembership?.identity.name,
|
||||
id: nextAvailableMembership?.identity.id
|
||||
}
|
||||
});
|
||||
handlePopUpToggle("identity", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -114,34 +134,41 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Identity to Project">
|
||||
<ModalContent title="Add Identity to Project" bodyClassName="overflow-visible">
|
||||
{filteredIdentityMembershipOrgs.length ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="identityId"
|
||||
defaultValue={filteredIdentityMembershipOrgs?.[0]?.id}
|
||||
name="identity"
|
||||
defaultValue={{
|
||||
id: filteredIdentityMembershipOrgs?.[0]?.identity?.id,
|
||||
name: filteredIdentityMembershipOrgs?.[0]?.identity?.name
|
||||
}}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
<ComboBox
|
||||
className="w-full"
|
||||
>
|
||||
{filteredIdentityMembershipOrgs.map(({ identity }) => (
|
||||
<SelectItem value={identity.id} key={`org-identity-${identity.id}`}>
|
||||
{identity.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
by="id"
|
||||
value={{ id: field.value.id, name: field.value.name }}
|
||||
defaultValue={{ id: field.value.id, name: field.value.name }}
|
||||
onSelectChange={(value) => onChange({ id: value.id, name: value.name })}
|
||||
displayValue={(el) => el.name}
|
||||
onFilter={({ value }, filterQuery) =>
|
||||
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||
}
|
||||
items={filteredIdentityMembershipOrgs.map(({ identity }) => ({
|
||||
key: identity.id,
|
||||
value: { id: identity.id, name: identity.name },
|
||||
label: identity.name
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
defaultValue=""
|
||||
defaultValue={{ name: "", slug: "" }}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
@@ -149,18 +176,22 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
<ComboBox
|
||||
className="w-full"
|
||||
>
|
||||
{(roles || []).map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
by="slug"
|
||||
value={{ slug: field.value.slug, name: field.value.name }}
|
||||
defaultValue={{ slug: field.value.slug, name: field.value.name }}
|
||||
onSelectChange={(value) => onChange({ slug: value.slug, name: value.name })}
|
||||
displayValue={(el) => el.name}
|
||||
onFilter={({ value }, filterQuery) =>
|
||||
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||
}
|
||||
items={(roles || []).map(({ slug, name }) => ({
|
||||
key: slug,
|
||||
value: { slug, name },
|
||||
label: name
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -187,7 +218,9 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
All identities in your organization have already been added to this project.
|
||||
</div>
|
||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||
<Button variant="outline_bg">Create a new identity</Button>
|
||||
<Button isDisabled={isRolesLoading} isLoading={isRolesLoading} variant="outline_bg">
|
||||
Create a new identity
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user