mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 17:27:16 +00:00
feat: added a temporary combobox for identity addition to project
This commit is contained in:
@@ -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 MyComboBoxProps<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
|
||||||
|
}: MyComboBoxProps<T>) => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const filteredResult =
|
||||||
|
query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query));
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ComboBox } from "./ComboBox";
|
||||||
@@ -11,13 +11,24 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
|||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
subTitle?: ReactNode;
|
subTitle?: ReactNode;
|
||||||
footerContent?: ReactNode;
|
footerContent?: ReactNode;
|
||||||
|
bodyClassName?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
overlayClassName?: string;
|
overlayClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||||
(
|
(
|
||||||
{ children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props },
|
{
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
className,
|
||||||
|
overlayClassName,
|
||||||
|
footerContent,
|
||||||
|
bodyClassName,
|
||||||
|
onClose,
|
||||||
|
...props
|
||||||
|
},
|
||||||
forwardedRef
|
forwardedRef
|
||||||
) => (
|
) => (
|
||||||
<DialogPrimitive.Portal>
|
<DialogPrimitive.Portal>
|
||||||
@@ -35,7 +46,10 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
|||||||
style={{ maxHeight: "90%" }}
|
style={{ maxHeight: "90%" }}
|
||||||
>
|
>
|
||||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
{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}
|
{children}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
||||||
|
|||||||
+50
-40
@@ -5,15 +5,8 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||||
Button,
|
import { ComboBox } from "@app/components/v2/ComboBox";
|
||||||
FormControl,
|
|
||||||
Modal,
|
|
||||||
ModalClose,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
|
||||||
SelectItem
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityToWorkspace,
|
useAddIdentityToWorkspace,
|
||||||
@@ -25,8 +18,14 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
const schema = yup
|
const schema = yup
|
||||||
.object({
|
.object({
|
||||||
identityId: yup.string().required("Identity id is required"),
|
identity: yup.object({
|
||||||
role: yup.string()
|
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();
|
.required();
|
||||||
|
|
||||||
@@ -79,12 +78,12 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
resolver: yupResolver(schema)
|
resolver: yupResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = async ({ identityId, role }: FormData) => {
|
const onFormSubmit = async ({ identity, role }: FormData) => {
|
||||||
try {
|
try {
|
||||||
await addIdentityToWorkspaceMutateAsync({
|
await addIdentityToWorkspaceMutateAsync({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
identityId,
|
identityId: identity.id,
|
||||||
role: role || undefined
|
role: role.slug || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -114,34 +113,41 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title="Add Identity to Project">
|
<ModalContent title="Add Identity to Project" bodyClassName="overflow-hidden">
|
||||||
{filteredIdentityMembershipOrgs.length ? (
|
{filteredIdentityMembershipOrgs.length ? (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="identityId"
|
name="identity"
|
||||||
defaultValue={filteredIdentityMembershipOrgs?.[0]?.id}
|
defaultValue={{
|
||||||
|
id: filteredIdentityMembershipOrgs?.[0]?.id,
|
||||||
|
name: filteredIdentityMembershipOrgs?.[0]?.identity?.name
|
||||||
|
}}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||||
<Select
|
<ComboBox
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
by="id"
|
||||||
{filteredIdentityMembershipOrgs.map(({ identity }) => (
|
value={{ id: field.value.id, name: field.value.name }}
|
||||||
<SelectItem value={identity.id} key={`org-identity-${identity.id}`}>
|
defaultValue={{ id: field.value.id, name: field.value.name }}
|
||||||
{identity.name}
|
onSelectChange={(value) => onChange({ id: value.id, name: value.name })}
|
||||||
</SelectItem>
|
displayValue={(el) => el.name}
|
||||||
))}
|
onFilter={({ value }, filterQuery) =>
|
||||||
</Select>
|
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||||
|
}
|
||||||
|
items={filteredIdentityMembershipOrgs.map(({ identity }) => ({
|
||||||
|
key: identity.id,
|
||||||
|
value: { id: identity.id, name: identity.name },
|
||||||
|
label: identity.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="role"
|
name="role"
|
||||||
defaultValue=""
|
defaultValue={{ name: "", slug: "" }}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Role"
|
label="Role"
|
||||||
@@ -149,18 +155,22 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
>
|
>
|
||||||
<Select
|
<ComboBox
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
by="slug"
|
||||||
{(roles || []).map(({ name, slug }) => (
|
value={{ slug: field.value.slug, name: field.value.name }}
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
defaultValue={{ slug: field.value.slug, name: field.value.name }}
|
||||||
{name}
|
onSelectChange={(value) => onChange({ slug: value.slug, name: value.name })}
|
||||||
</SelectItem>
|
displayValue={(el) => el.name}
|
||||||
))}
|
onFilter={({ value }, filterQuery) =>
|
||||||
</Select>
|
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||||
|
}
|
||||||
|
items={(roles || []).map(({ slug, name }) => ({
|
||||||
|
key: slug,
|
||||||
|
value: { slug, name },
|
||||||
|
label: name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user