feat: added a temporary combobox for identity addition to project

This commit is contained in:
=
2024-09-13 00:29:47 +05:30
parent 53b9fe2dec
commit 1c75fc84f0
4 changed files with 150 additions and 42 deletions

View 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 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>
);
};

View File

@@ -0,0 +1 @@
export { ComboBox } from "./ComboBox";

View File

@@ -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>}

View File

@@ -5,15 +5,8 @@ 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();
@@ -79,12 +78,12 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
resolver: yupResolver(schema)
});
const onFormSubmit = async ({ identityId, role }: FormData) => {
const onFormSubmit = async ({ identity, role }: FormData) => {
try {
await addIdentityToWorkspaceMutateAsync({
workspaceId,
identityId,
role: role || undefined
identityId: identity.id,
role: role.slug || undefined
});
createNotification({
@@ -114,34 +113,41 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
reset();
}}
>
<ModalContent title="Add Identity to Project">
<ModalContent title="Add Identity to Project" bodyClassName="overflow-hidden">
{filteredIdentityMembershipOrgs.length ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="identityId"
defaultValue={filteredIdentityMembershipOrgs?.[0]?.id}
name="identity"
defaultValue={{
id: filteredIdentityMembershipOrgs?.[0]?.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 +155,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>
)}
/>