From 0b19b76b59a3800292a98717f87246d139a5c584 Mon Sep 17 00:00:00 2001 From: Akbar Abdrakhmanov Date: Fri, 6 Sep 2024 19:39:39 +0500 Subject: [PATCH 1/3] set owner and reviewers on each webpage --- static/client/App.scss | 7 ++ .../CustomSearchAndFilter.tsx | 84 +++++++++++++++++++ .../components/OwnerAndReviewers/Owner.tsx | 49 +++++++++++ .../OwnerAndReviewers.hooks.ts | 27 ++++++ .../OwnerAndReviewers/OwnerAndReviewers.tsx | 12 +++ .../OwnerAndReviewers.types.ts | 24 ++++++ .../OwnerAndReviewers/Reviewers.tsx | 54 ++++++++++++ .../components/OwnerAndReviewers/index.ts | 1 + static/client/components/Search/Search.tsx | 40 ++++----- static/client/components/Search/_Search.scss | 3 +- static/client/pages/Webpage/Webpage.tsx | 16 ++-- static/client/services/api/constants.ts | 4 +- static/client/services/api/index.ts | 6 +- .../services/api/partials/AuthApiClass.ts | 9 -- .../services/api/partials/BasicApiClass.ts | 2 +- .../services/api/partials/PagesApiClass.ts | 15 ++++ .../services/api/partials/UsersApiClass.ts | 10 +++ static/client/services/api/services/auth.ts | 7 -- static/client/services/api/services/pages.ts | 9 ++ static/client/services/api/services/users.ts | 8 ++ static/client/services/api/services/utils.ts | 22 +++++ static/client/services/api/types/pages.ts | 5 ++ static/client/services/api/types/users.ts | 12 +++ 23 files changed, 379 insertions(+), 47 deletions(-) create mode 100644 static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx create mode 100644 static/client/components/OwnerAndReviewers/Owner.tsx create mode 100644 static/client/components/OwnerAndReviewers/OwnerAndReviewers.hooks.ts create mode 100644 static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx create mode 100644 static/client/components/OwnerAndReviewers/OwnerAndReviewers.types.ts create mode 100644 static/client/components/OwnerAndReviewers/Reviewers.tsx create mode 100644 static/client/components/OwnerAndReviewers/index.ts delete mode 100644 static/client/services/api/partials/AuthApiClass.ts create mode 100644 static/client/services/api/partials/UsersApiClass.ts delete mode 100644 static/client/services/api/services/auth.ts create mode 100644 static/client/services/api/services/users.ts create mode 100644 static/client/services/api/services/utils.ts create mode 100644 static/client/services/api/types/users.ts diff --git a/static/client/App.scss b/static/client/App.scss index b750ab0d..be7f5caa 100644 --- a/static/client/App.scss +++ b/static/client/App.scss @@ -13,6 +13,8 @@ @include vf-p-side-navigation; @include vf-p-buttons; @include vf-p-tooltips; +@include vf-p-chip; +@include vf-p-search-and-filter; // Icons @include vf-p-icons; @@ -47,3 +49,8 @@ @extend %icon; @include vf-icon-logout(#ffffff); } + +// allow SearchAndFilter component increase height when there are several options selected (in Reviewers) +.p-search-and-filter__search-container { + height: auto !important; +} diff --git a/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx b/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx new file mode 100644 index 00000000..997d8868 --- /dev/null +++ b/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx @@ -0,0 +1,84 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import type { ICustomSearchAndFilterProps } from "./OwnerAndReviewers.types"; + +import type { IUser } from "@/services/api/types/users"; + +const CustomSearchAndFilter = ({ + label, + options, + selectedOptions, + placeholder, + onChange, + onRemove, + onSelect, +}: ICustomSearchAndFilterProps): JSX.Element => { + const [dropdownHidden, setDropdownHidden] = useState(true); + const [containerExpanded, setContainerExpanded] = useState(false); + + const inputRef = useRef(null); + + useEffect(() => { + setDropdownHidden(!options.length); + setContainerExpanded(!!options.length); + }, [options]); + + const handleSelect = useCallback( + (option: IUser) => () => { + onSelect(option); + if (inputRef.current) { + inputRef.current.value = ""; + } + }, + [onSelect], + ); + + return ( +
+

+ {label} +

+
+ {selectedOptions.map((option) => ( + + {option.name} + + + ))} +
+ +
+
+
+
+ +
+
+
+ ); +}; + +export default CustomSearchAndFilter; diff --git a/static/client/components/OwnerAndReviewers/Owner.tsx b/static/client/components/OwnerAndReviewers/Owner.tsx new file mode 100644 index 00000000..49d1f56b --- /dev/null +++ b/static/client/components/OwnerAndReviewers/Owner.tsx @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useState } from "react"; + +import CustomSearchAndFilter from "./CustomSearchAndFilter"; +import { useUsersRequest } from "./OwnerAndReviewers.hooks"; +import type { IOwnerAndReviewersProps } from "./OwnerAndReviewers.types"; + +import { PagesServices } from "@/services/api/services/pages"; +import { type IUser } from "@/services/api/types/users"; + +const Owner = ({ page }: IOwnerAndReviewersProps): JSX.Element => { + const [currentOwner, setCurrentOwner] = useState(null); + const { options, setOptions, handleChange } = useUsersRequest(); + + useEffect(() => { + setCurrentOwner(page.owner); + }, [page]); + + const handleRemoveOwner = useCallback( + () => () => { + setCurrentOwner(null); + }, + [], + ); + + const selectOwner = useCallback( + (option: IUser) => { + PagesServices.setOwner(option, page.id); + setOptions([]); + setCurrentOwner(option); + // mutate the tree structure element to reflect the recent change + page.owner = option; + }, + [page, setOptions], + ); + + return ( + + ); +}; + +export default Owner; diff --git a/static/client/components/OwnerAndReviewers/OwnerAndReviewers.hooks.ts b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.hooks.ts new file mode 100644 index 00000000..852054c3 --- /dev/null +++ b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.hooks.ts @@ -0,0 +1,27 @@ +import { type ChangeEvent, useCallback, useState } from "react"; + +import type { IUseUsersRequest } from "./OwnerAndReviewers.types"; + +import { UsersServices } from "@/services/api/services/users"; +import { UtilServices } from "@/services/api/services/utils"; +import type { IUser } from "@/services/api/types/users"; + +const debouncedRequest = UtilServices.debounce(UsersServices.getUsers, 500); + +export const useUsersRequest = (): IUseUsersRequest => { + const [options, setOptions] = useState([]); + + const handleChange = useCallback(async (event: ChangeEvent) => { + const { value } = event.target; + if (value.length >= 2) { + const users = await debouncedRequest(value); + if (users?.data?.length) { + setOptions(users.data); + } + } else { + setOptions([]); + } + }, []); + + return { options, setOptions, handleChange }; +}; diff --git a/static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx new file mode 100644 index 00000000..d364808b --- /dev/null +++ b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx @@ -0,0 +1,12 @@ +import Owner from "./Owner"; +import type { IOwnerAndReviewersProps } from "./OwnerAndReviewers.types"; +import Reviewers from "./Reviewers"; + +const OwnerAndReviewers = ({ page }: IOwnerAndReviewersProps): JSX.Element => ( + <> + + + +); + +export default OwnerAndReviewers; diff --git a/static/client/components/OwnerAndReviewers/OwnerAndReviewers.types.ts b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.types.ts new file mode 100644 index 00000000..d9b508c7 --- /dev/null +++ b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.types.ts @@ -0,0 +1,24 @@ +import type { ChangeEvent, Dispatch, SetStateAction } from "react"; + +import type { IPage } from "@/services/api/types/pages"; +import type { IUser } from "@/services/api/types/users"; + +export interface IOwnerAndReviewersProps { + page: IPage; +} + +export interface ICustomSearchAndFilterProps { + label: string; + options: IUser[]; + selectedOptions: IUser[]; + placeholder: string; + onChange: (event: ChangeEvent) => void; + onRemove: (option?: IUser) => () => void; + onSelect: (option: IUser) => void; +} + +export interface IUseUsersRequest { + options: IUser[]; + setOptions: Dispatch>; + handleChange: (event: ChangeEvent) => void; +} diff --git a/static/client/components/OwnerAndReviewers/Reviewers.tsx b/static/client/components/OwnerAndReviewers/Reviewers.tsx new file mode 100644 index 00000000..a9c01963 --- /dev/null +++ b/static/client/components/OwnerAndReviewers/Reviewers.tsx @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useState } from "react"; + +import CustomSearchAndFilter from "./CustomSearchAndFilter"; +import { useUsersRequest } from "./OwnerAndReviewers.hooks"; +import type { IOwnerAndReviewersProps } from "./OwnerAndReviewers.types"; + +import { PagesServices } from "@/services/api/services/pages"; +import { type IUser } from "@/services/api/types/users"; + +const Reviewers = ({ page }: IOwnerAndReviewersProps): JSX.Element => { + const [currentReviewers, setCurrentReviewers] = useState([]); + const { options, setOptions, handleChange } = useUsersRequest(); + + useEffect(() => { + setCurrentReviewers(page.reviewers); + }, [page]); + + const handleRemoveReviewer = useCallback( + (option?: IUser) => () => { + setCurrentReviewers((prev) => prev.filter((r) => r.id !== option?.id)); + }, + [], + ); + + const selectReviewer = useCallback( + (option: IUser) => { + if (!currentReviewers.find((r) => r.id === option.id)) { + const newReviewers = [...currentReviewers, option]; + PagesServices.setReviewers(newReviewers, page.id); + setOptions([]); + setCurrentReviewers(newReviewers); + // mutate the tree structure element to reflect the recent change + page.reviewers = newReviewers; + } + }, + [page, currentReviewers, setOptions], + ); + + return ( + <> + + + ); +}; + +export default Reviewers; diff --git a/static/client/components/OwnerAndReviewers/index.ts b/static/client/components/OwnerAndReviewers/index.ts new file mode 100644 index 00000000..bb06f98f --- /dev/null +++ b/static/client/components/OwnerAndReviewers/index.ts @@ -0,0 +1 @@ +export { default } from "./OwnerAndReviewers"; diff --git a/static/client/components/Search/Search.tsx b/static/client/components/Search/Search.tsx index 94b08b63..881a496b 100644 --- a/static/client/components/Search/Search.tsx +++ b/static/client/components/Search/Search.tsx @@ -46,26 +46,26 @@ const Search = (): JSX.Element => { ); return ( -
- { - - } - {matches.length >= 0 && ( -
    - {matches.map((match) => ( -
  • - {match.name} - {match.title} -
  • - ))} -
- )} -
+ <> + +
+ {matches.length >= 0 && ( +
    + {matches.map((match) => ( +
  • + {match.name} - {match.title} +
  • + ))} +
+ )} +
+ ); }; diff --git a/static/client/components/Search/_Search.scss b/static/client/components/Search/_Search.scss index c530c0ad..1ecff2ab 100644 --- a/static/client/components/Search/_Search.scss +++ b/static/client/components/Search/_Search.scss @@ -9,9 +9,10 @@ list-style-type: none; margin: 0; max-height: calc(100vh - 200px); + overflow: auto; padding: 0; position: absolute; - top: 50px; + top: -10px; width: 100%; z-index: 10; diff --git a/static/client/pages/Webpage/Webpage.tsx b/static/client/pages/Webpage/Webpage.tsx index 9564397a..9b8297fa 100644 --- a/static/client/pages/Webpage/Webpage.tsx +++ b/static/client/pages/Webpage/Webpage.tsx @@ -4,6 +4,7 @@ import { Button } from "@canonical/react-components"; import { type IWebpageProps } from "./Webpage.types"; +import OwnerAndReviewers from "@/components/OwnerAndReviewers"; import config from "@/config"; const Webpage = ({ page, project }: IWebpageProps): JSX.Element => { @@ -49,11 +50,16 @@ const Webpage = ({ page, project }: IWebpageProps): JSX.Element => { )} -
-

- Description -

-

{page.description || "-"}

+
+
+

+ Description +

+

{page.description || "-"}

+
+
+ +
); diff --git a/static/client/services/api/constants.ts b/static/client/services/api/constants.ts index 7138447f..e3bb68ff 100644 --- a/static/client/services/api/constants.ts +++ b/static/client/services/api/constants.ts @@ -1,6 +1,8 @@ export const ENDPOINTS = { getPagesTree: (domain: string) => `/get-tree/${domain}`, - login: (url: string) => `/login?next=${url}`, + getUsers: (inputStr: string) => `/get-users/${inputStr}`, + setOwner: "/set-owner", + setReviewers: "/set-reviewers", }; export const REST_TYPES = { diff --git a/static/client/services/api/index.ts b/static/client/services/api/index.ts index 17405d88..4dd9bc8e 100644 --- a/static/client/services/api/index.ts +++ b/static/client/services/api/index.ts @@ -1,13 +1,13 @@ -import { AuthApiClass } from "./partials/AuthApiClass"; import { PagesApiClass } from "./partials/PagesApiClass"; +import { UsersApiClass } from "./partials/UsersApiClass"; class ApiClass { public pages: PagesApiClass; - public auth: AuthApiClass; + public users: UsersApiClass; constructor() { this.pages = new PagesApiClass(); - this.auth = new AuthApiClass(); + this.users = new UsersApiClass(); } } diff --git a/static/client/services/api/partials/AuthApiClass.ts b/static/client/services/api/partials/AuthApiClass.ts deleted file mode 100644 index 5d3317b8..00000000 --- a/static/client/services/api/partials/AuthApiClass.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BasicApiClass } from "./BasicApiClass"; - -import { ENDPOINTS, REST_TYPES } from "@/services/api/constants"; - -export class AuthApiClass extends BasicApiClass { - public login(url: string) { - return this.callApi(ENDPOINTS.login(url), REST_TYPES.GET); - } -} diff --git a/static/client/services/api/partials/BasicApiClass.ts b/static/client/services/api/partials/BasicApiClass.ts index d7aff360..dd97d264 100644 --- a/static/client/services/api/partials/BasicApiClass.ts +++ b/static/client/services/api/partials/BasicApiClass.ts @@ -13,7 +13,7 @@ export class BasicApiClass { this.timeout = 5 * 60 * 1000; this.headers = { Accept: "application/json", - "Content-Type": "application/x-www-form-urlencoded", + "Content-Type": "application/json", }; } diff --git a/static/client/services/api/partials/PagesApiClass.ts b/static/client/services/api/partials/PagesApiClass.ts index c00daa00..402268b1 100644 --- a/static/client/services/api/partials/PagesApiClass.ts +++ b/static/client/services/api/partials/PagesApiClass.ts @@ -2,9 +2,24 @@ import { BasicApiClass } from "./BasicApiClass"; import { ENDPOINTS, REST_TYPES } from "@/services/api/constants"; import type { IPagesResponse } from "@/services/api/types/pages"; +import { type IUser } from "@/services/api/types/users"; export class PagesApiClass extends BasicApiClass { public getPages(domain: string): Promise { return this.callApi(ENDPOINTS.getPagesTree(domain), REST_TYPES.GET); } + + public setOwner(user: IUser, webpageId: number): Promise { + return this.callApi(ENDPOINTS.setOwner, REST_TYPES.POST, { + user_struct: user, + webpage_id: webpageId, + }); + } + + public setReviewers(users: IUser[], webpageId: number): Promise { + return this.callApi(ENDPOINTS.setReviewers, REST_TYPES.POST, { + user_structs: users, + webpage_id: webpageId, + }); + } } diff --git a/static/client/services/api/partials/UsersApiClass.ts b/static/client/services/api/partials/UsersApiClass.ts new file mode 100644 index 00000000..bcbfa8cf --- /dev/null +++ b/static/client/services/api/partials/UsersApiClass.ts @@ -0,0 +1,10 @@ +import { BasicApiClass } from "./BasicApiClass"; + +import { ENDPOINTS, REST_TYPES } from "@/services/api/constants"; +import { type IUsersResponse } from "@/services/api/types/users"; + +export class UsersApiClass extends BasicApiClass { + public getUsers(username: string): Promise { + return this.callApi(ENDPOINTS.getUsers(username), REST_TYPES.GET); + } +} diff --git a/static/client/services/api/services/auth.ts b/static/client/services/api/services/auth.ts deleted file mode 100644 index fef9134d..00000000 --- a/static/client/services/api/services/auth.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { api } from ".."; - -export const login = (url: string) => { - return api.auth.login(url); -}; - -export * as AuthServices from "./auth"; diff --git a/static/client/services/api/services/pages.ts b/static/client/services/api/services/pages.ts index 5548d78d..7549757e 100644 --- a/static/client/services/api/services/pages.ts +++ b/static/client/services/api/services/pages.ts @@ -1,8 +1,17 @@ import { api } from "@/services/api"; import type { IPagesResponse } from "@/services/api/types/pages"; +import type { IUser } from "@/services/api/types/users"; export const getPages = async (domain: string): Promise => { return api.pages.getPages(domain); }; +export const setOwner = async (user: IUser, webpageId: number): Promise => { + return api.pages.setOwner(user, webpageId); +}; + +export const setReviewers = async (users: IUser[], webpageId: number): Promise => { + return api.pages.setReviewers(users, webpageId); +}; + export * as PagesServices from "./pages"; diff --git a/static/client/services/api/services/users.ts b/static/client/services/api/services/users.ts new file mode 100644 index 00000000..a12a76c0 --- /dev/null +++ b/static/client/services/api/services/users.ts @@ -0,0 +1,8 @@ +import { api } from "@/services/api"; +import type { IUsersResponse } from "@/services/api/types/users"; + +export const getUsers = async (username: string): Promise => { + return api.users.getUsers(username); +}; + +export * as UsersServices from "./users"; diff --git a/static/client/services/api/services/utils.ts b/static/client/services/api/services/utils.ts new file mode 100644 index 00000000..a5a9cba2 --- /dev/null +++ b/static/client/services/api/services/utils.ts @@ -0,0 +1,22 @@ +export function debounce(func: (arg: string) => Promise, wait: number): (arg: string) => Promise { + let timeout: NodeJS.Timeout | undefined; + + return function (arg: string): Promise { + if (timeout) { + clearTimeout(timeout); + } + + return new Promise((resolve, reject) => { + timeout = setTimeout(async () => { + try { + const result = await func(arg); + resolve(result); + } catch (error) { + reject(error); + } + }, wait); + }); + }; +} + +export * as UtilServices from "./utils"; diff --git a/static/client/services/api/types/pages.ts b/static/client/services/api/types/pages.ts index 9e85604c..ef54f37c 100644 --- a/static/client/services/api/types/pages.ts +++ b/static/client/services/api/types/pages.ts @@ -1,8 +1,13 @@ +import type { IUser } from "./users"; + export interface IPage { + id: number; name: string; title: string; description: string; link: string; + owner: IUser; + reviewers: IUser[]; children: IPage[]; } diff --git a/static/client/services/api/types/users.ts b/static/client/services/api/types/users.ts new file mode 100644 index 00000000..176beaea --- /dev/null +++ b/static/client/services/api/types/users.ts @@ -0,0 +1,12 @@ +export interface IUser { + id: number; + name: string; + email: string; + jobTitle: string; + department: string; + team: string; +} + +export interface IUsersResponse { + data: IUser[]; +} From 8425d3aa9d5dd48fd905787f2f6a0cc6dab85b3c Mon Sep 17 00:00:00 2001 From: Akbar Abdrakhmanov Date: Fri, 6 Sep 2024 20:31:12 +0500 Subject: [PATCH 2/3] do not allow setting same person as a reviewer --- .../CustomSearchAndFilter.tsx | 1 + .../OwnerAndReviewers/OwnerAndReviewers.tsx | 1 + .../OwnerAndReviewers/Reviewers.tsx | 24 +++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx b/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx index 997d8868..85329cf6 100644 --- a/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx +++ b/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx @@ -1,3 +1,4 @@ +// the existing SearchAndFilter component provided by react-components did not provide ability to have dynamic options import { useCallback, useEffect, useRef, useState } from "react"; import type { ICustomSearchAndFilterProps } from "./OwnerAndReviewers.types"; diff --git a/static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx index d364808b..d8c81260 100644 --- a/static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx +++ b/static/client/components/OwnerAndReviewers/OwnerAndReviewers.tsx @@ -5,6 +5,7 @@ import Reviewers from "./Reviewers"; const OwnerAndReviewers = ({ page }: IOwnerAndReviewersProps): JSX.Element => ( <> +
); diff --git a/static/client/components/OwnerAndReviewers/Reviewers.tsx b/static/client/components/OwnerAndReviewers/Reviewers.tsx index a9c01963..4c537527 100644 --- a/static/client/components/OwnerAndReviewers/Reviewers.tsx +++ b/static/client/components/OwnerAndReviewers/Reviewers.tsx @@ -24,7 +24,9 @@ const Reviewers = ({ page }: IOwnerAndReviewersProps): JSX.Element => { const selectReviewer = useCallback( (option: IUser) => { - if (!currentReviewers.find((r) => r.id === option.id)) { + // check if a person with the same email already exists + // proceed with setting the reviewer only if not + if (!currentReviewers.find((r) => r.email === option.email)) { const newReviewers = [...currentReviewers, option]; PagesServices.setReviewers(newReviewers, page.id); setOptions([]); @@ -37,17 +39,15 @@ const Reviewers = ({ page }: IOwnerAndReviewersProps): JSX.Element => { ); return ( - <> - - + ); }; From 81df6bd32fcd127b65f825c8828e0b60dc2e7d55 Mon Sep 17 00:00:00 2001 From: Akbar Abdrakhmanov Date: Wed, 11 Sep 2024 11:34:24 +0500 Subject: [PATCH 3/3] allow removing owner and reviewers, close dropdown when not focused on input --- .../CustomSearchAndFilter.tsx | 19 +++++++++++++-- .../components/OwnerAndReviewers/Owner.tsx | 3 ++- .../OwnerAndReviewers/Reviewers.tsx | 6 +++-- static/client/components/Search/Search.tsx | 23 +++++++++++++++---- .../services/api/partials/PagesApiClass.ts | 2 +- static/client/services/api/services/pages.ts | 2 +- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx b/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx index 85329cf6..f8cb91aa 100644 --- a/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx +++ b/static/client/components/OwnerAndReviewers/CustomSearchAndFilter.tsx @@ -1,5 +1,5 @@ // the existing SearchAndFilter component provided by react-components did not provide ability to have dynamic options -import { useCallback, useEffect, useRef, useState } from "react"; +import { type MouseEvent, useCallback, useEffect, useRef, useState } from "react"; import type { ICustomSearchAndFilterProps } from "./OwnerAndReviewers.types"; @@ -34,6 +34,20 @@ const CustomSearchAndFilter = ({ [onSelect], ); + // a callback that closes an options dropdown when focus is not on an input field + const handleInputBlur = useCallback(() => { + setDropdownHidden(true); + setContainerExpanded(false); + if (inputRef.current) { + inputRef.current.value = ""; + } + }, []); + + // this callback is needed for handleSelect to have higher priority than handleInputBlur when selecting an option + const handleOptionMouseDown = useCallback((event: MouseEvent) => { + event.preventDefault(); + }, []); + return (

@@ -60,6 +74,7 @@ const CustomSearchAndFilter = ({ className="p-search-and-filter__input" id="search" name="search" + onBlur={handleInputBlur} onChange={onChange} placeholder={placeholder} ref={inputRef} @@ -71,7 +86,7 @@ const CustomSearchAndFilter = ({