diff --git a/apps/app/components/issues/select/assignee.tsx b/apps/app/components/issues/select/assignee.tsx index 42f7063ae..f932e0e6c 100644 --- a/apps/app/components/issues/select/assignee.tsx +++ b/apps/app/components/issues/select/assignee.tsx @@ -1,35 +1,23 @@ -import { useState, FC, Fragment } from "react"; - import { useRouter } from "next/router"; import useSWR from "swr"; -// headless ui -import { Transition, Combobox } from "@headlessui/react"; // services import projectServices from "services/project.service"; // ui -import { AssigneesList, Avatar } from "components/ui"; +import { AssigneesList, Avatar, CustomSearchSelect } from "components/ui"; // icons -import { UserGroupIcon, MagnifyingGlassIcon, CheckIcon } from "@heroicons/react/24/outline"; - -// fetch keys +import { UserGroupIcon } from "@heroicons/react/24/outline"; +// fetch-keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; -export type IssueAssigneeSelectProps = { +export type Props = { projectId: string; value: string[]; onChange: (value: string[]) => void; }; -export const IssueAssigneeSelect: FC = ({ - projectId, - value = [], - onChange, -}) => { - // states - const [query, setQuery] = useState(""); - +export const IssueAssigneeSelect: React.FC = ({ projectId, value = [], onChange }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -41,120 +29,44 @@ export const IssueAssigneeSelect: FC = ({ : null ); - const options = people?.map((person) => ({ - value: person.member.id, - display: - person.member.first_name && person.member.first_name !== "" - ? person.member.first_name - : person.member.email, - })); - - const filteredOptions = - query === "" - ? options - : options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase())); + const options = + people?.map((person) => ({ + value: person.member.id, + query: + person.member.first_name && person.member.first_name !== "" + ? person.member.first_name + : person.member.email, + content: ( +
+ + {person.member.first_name && person.member.first_name !== "" + ? person.member.first_name + : person.member.email} +
+ ), + })) ?? []; return ( - onChange(val)} - className="relative flex-shrink-0" + onChange={onChange} + options={options} + label={ +
+ {value && value.length > 0 && Array.isArray(value) ? ( + + + {value.length} Assignees + + ) : ( + + + Assignee + + )} +
+ } multiple - > - {({ open }: any) => ( - <> - - `flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200 - ${ - open - ? "border-theme bg-theme/5 outline-none ring-1 ring-theme " - : "hover:bg-theme/5 " - }` - } - > - {value && value.length > 0 && Array.isArray(value) ? ( - - - {value.length} Assignees - - ) : ( - - - Assignee - - )} - - - - -
- - setQuery(event.target.value)} - placeholder="Search for a person..." - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `${ - active ? "bg-hover-gray" : "" - } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-500` - } - value={option.value} - > - {({ selected, active }) => ( -
-
- p.member.id === option.value)?.member} - /> - {option.display} -
-
- -
-
- )} -
- )) - ) : ( -

No assignees found

- ) - ) : ( -

Loading...

- )} -
-
-
- - )} -
+ /> ); }; diff --git a/apps/app/components/issues/select/state.tsx b/apps/app/components/issues/select/state.tsx index a14d384a2..f8a0c5e15 100644 --- a/apps/app/components/issues/select/state.tsx +++ b/apps/app/components/issues/select/state.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; @@ -6,20 +6,15 @@ import useSWR from "swr"; // services import stateService from "services/state.service"; -// headless ui -import { - Squares2X2Icon, - PlusIcon, - MagnifyingGlassIcon, - CheckIcon, -} from "@heroicons/react/24/outline"; +// ui +import { CustomSearchSelect } from "components/ui"; // icons -import { Combobox, Transition } from "@headlessui/react"; +import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import { getStateGroupIcon } from "components/icons"; // helpers import { getStatesList } from "helpers/state.helper"; // fetch keys import { STATE_LIST } from "constants/fetch-keys"; -import { getStateGroupIcon } from "components/icons"; type Props = { setIsOpen: React.Dispatch>; @@ -30,8 +25,6 @@ type Props = { export const IssueStateSelect: React.FC = ({ setIsOpen, value, onChange, projectId }) => { // states - const [query, setQuery] = useState(""); - const router = useRouter(); const { workspaceSlug } = router.query; @@ -45,123 +38,40 @@ export const IssueStateSelect: React.FC = ({ setIsOpen, value, onChange, const options = states?.map((state) => ({ value: state.id, - display: state.name, - color: state.color, - group: state.group, + query: state.name, + content: ( +
+ {getStateGroupIcon(state.group, "16", "16", state.color)} + {state.name} +
+ ), })); - const filteredOptions = - query === "" - ? options - : options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase())); + const selectedOption = states?.find((s) => s.id === value); - const currentOption = options?.find((option) => option.value === value); return ( - onChange(val)} - className="relative flex-shrink-0" - > - {({ open }: any) => ( - <> - - `flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200 - ${ - open ? "border-theme bg-theme/5 outline-none ring-1 ring-theme " : "hover:bg-theme/5" - }` - } - > - {value && value !== "" ? ( - - {currentOption && currentOption.group - ? getStateGroupIcon(currentOption.group, "16", "16", currentOption.color) - : ""} - {currentOption?.display} - - ) : ( - - - {currentOption?.display || "State"} - - )} - - - - -
- - setQuery(event.target.value)} - placeholder="Search States" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `${ - active ? "bg-gray-200" : "" - } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` - } - value={option.value} - > - {({ selected, active }) => - states && ( -
-
- {getStateGroupIcon(option.group, "16", "16", option.color)} - {option.display} -
-
- -
-
- ) - } -
- )) - ) : ( -

No states found

- ) - ) : ( -

Loading...

- )} - -
-
-
- - )} -
+ onChange={onChange} + options={options} + label={ +
+ + {selectedOption && + getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)} + {selectedOption?.name ?? "State"} +
+ } + footerOption={ + + } + /> ); }; diff --git a/apps/app/components/ui/custom-search-select.tsx b/apps/app/components/ui/custom-search-select.tsx new file mode 100644 index 000000000..b9266a8af --- /dev/null +++ b/apps/app/components/ui/custom-search-select.tsx @@ -0,0 +1,243 @@ +import React, { useState } from "react"; + +// headless ui +import { Combobox, Transition } from "@headlessui/react"; +// icons +import { CheckIcon, ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; + +type CustomSearchSelectProps = { + value: any; + onChange: any; + options: { + value: any; + query: string; + content: JSX.Element; + }[]; + label?: string | JSX.Element; + textAlignment?: "left" | "center" | "right"; + position?: "right" | "left"; + input?: boolean; + noChevron?: boolean; + customButton?: JSX.Element; + optionsClassName?: string; + disabled?: boolean; + selfPositioned?: boolean; + multiple?: boolean; + footerOption?: JSX.Element; +}; + +export const CustomSearchSelect = ({ + label, + textAlignment, + value, + onChange, + options, + position = "left", + input = false, + noChevron = false, + customButton, + optionsClassName = "", + disabled = false, + selfPositioned = false, + multiple = false, + footerOption, +}: CustomSearchSelectProps) => { + const [query, setQuery] = useState(""); + + const filteredOptions = + query === "" + ? options + : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + return ( + <> + {/* TODO: Improve this multiple logic */} + {multiple ? ( + + {({ open }: any) => ( + <> + {customButton ? ( + {customButton} + ) : ( + + {label} + {!noChevron && !disabled && ( + + )} + + + +
+ + setQuery(e.target.value)} + placeholder="Type to search..." + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `${active || selected ? "bg-hover-gray" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 text-gray-500` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+ {footerOption} +
+
+ + )} +
+ ) : ( + + {({ open }: any) => ( + <> + {customButton ? ( + {customButton} + ) : ( + + {label} + {!noChevron && !disabled && ( + + )} + + + +
+ + setQuery(e.target.value)} + placeholder="Type to search..." + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `${active || selected ? "bg-hover-gray" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 text-gray-500` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+ {footerOption} +
+
+ + )} +
+ )} + + ); +}; diff --git a/apps/app/components/ui/index.ts b/apps/app/components/ui/index.ts index 541a6fdb6..66f0ea1e5 100644 --- a/apps/app/components/ui/index.ts +++ b/apps/app/components/ui/index.ts @@ -3,6 +3,7 @@ export * from "./text-area"; export * from "./avatar"; export * from "./button"; export * from "./custom-menu"; +export * from "./custom-search-select"; export * from "./custom-select"; export * from "./datepicker"; export * from "./empty-space";