feat: added a temporary combobox for identity addition to project

This commit is contained in:
=
2024-09-13 00:35:48 +05:30
parent 53b9fe2dec
commit 1c75fc84f0
4 changed files with 150 additions and 42 deletions
@@ -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";
+16 -2
View File
@@ -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>}
@@ -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>
)} )}
/> />