From 1c75fc84f0dadc5e90497461ca0b7b17276ba13b Mon Sep 17 00:00:00 2001 From: = Date: Fri, 13 Sep 2024 00:29:47 +0530 Subject: [PATCH] feat: added a temporary combobox for identity addition to project --- .../src/components/v2/ComboBox/ComboBox.tsx | 83 +++++++++++++++++ frontend/src/components/v2/ComboBox/index.tsx | 1 + frontend/src/components/v2/Modal/Modal.tsx | 18 +++- .../IdentityTab/components/IdentityModal.tsx | 90 ++++++++++--------- 4 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/v2/ComboBox/ComboBox.tsx create mode 100644 frontend/src/components/v2/ComboBox/index.tsx diff --git a/frontend/src/components/v2/ComboBox/ComboBox.tsx b/frontend/src/components/v2/ComboBox/ComboBox.tsx new file mode 100644 index 000000000..692d19e63 --- /dev/null +++ b/frontend/src/components/v2/ComboBox/ComboBox.tsx @@ -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 = { + value?: T; + className?: string; + items: { + value: T; + key: string; + label: string; + }[]; + by: ByComparator; + 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 = ({ + onSelectChange, + onFilter, + displayValue, + by, + items, + ...props +}: MyComboBoxProps) => { + const [query, setQuery] = useState(""); + + const filteredResult = + query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)); + + return ( + +
+ 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" + /> + + + setQuery("")} + > + + {filteredResult.map(({ value, key, label }) => ( + + `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 ? ( +
+ +
+ ) : null} + + )} +
+ ))} +
+
+
+
+ ); +}; diff --git a/frontend/src/components/v2/ComboBox/index.tsx b/frontend/src/components/v2/ComboBox/index.tsx new file mode 100644 index 000000000..912296adf --- /dev/null +++ b/frontend/src/components/v2/ComboBox/index.tsx @@ -0,0 +1 @@ +export { ComboBox } from "./ComboBox"; diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx index 90abfecbd..a741d6d02 100644 --- a/frontend/src/components/v2/Modal/Modal.tsx +++ b/frontend/src/components/v2/Modal/Modal.tsx @@ -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( ( - { children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props }, + { + children, + title, + subTitle, + className, + overlayClassName, + footerContent, + bodyClassName, + onClose, + ...props + }, forwardedRef ) => ( @@ -35,7 +46,10 @@ export const ModalContent = forwardRef( style={{ maxHeight: "90%" }} > {title && {title}} - + {children} {footerContent && {footerContent}} diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx index d8c554524..0ca6f70f1 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx @@ -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(); }} > - + {filteredIdentityMembershipOrgs.length ? (
( - + 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 + }))} + /> )} /> ( { isError={Boolean(error)} className="mt-4" > - + 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 + }))} + /> )} />