From bcca90b9794864bc5f14fbda18d4d7981bab5fd6 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 26 May 2025 16:26:54 +0100 Subject: [PATCH] fix(frontend): resolve useSearch hook flickering --- frontend/src/components/inbox/index.tsx | 16 ++-- frontend/src/hooks/useQueryParam.ts | 76 ++++++++++++++++++ .../src/hooks/{useSearch.tsx => useSearch.ts} | 80 ++++++++++--------- frontend/src/types/search.types.ts | 5 +- 4 files changed, 128 insertions(+), 49 deletions(-) create mode 100644 frontend/src/hooks/useQueryParam.ts rename frontend/src/hooks/{useSearch.tsx => useSearch.ts} (55%) diff --git a/frontend/src/components/inbox/index.tsx b/frontend/src/components/inbox/index.tsx index e57e8192..71e15a0f 100644 --- a/frontend/src/components/inbox/index.tsx +++ b/frontend/src/components/inbox/index.tsx @@ -6,12 +6,13 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { MainContainer, Search, Sidebar } from "@chatscope/chat-ui-kit-react"; +import { MainContainer, Sidebar } from "@chatscope/chat-ui-kit-react"; import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; import { Grid, MenuItem } from "@mui/material"; import { useState } from "react"; import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; +import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { Input } from "@/app-components/inputs/Input"; import { useSearch } from "@/hooks/useSearch"; import { useTranslate } from "@/hooks/useTranslate"; @@ -26,8 +27,9 @@ import { AssignedTo } from "./types"; export const Inbox = () => { const { t } = useTranslate(); - const { onSearch, searchPayload, searchText } = useSearch({ + const { ref, onSearch, searchPayload } = useSearch({ $or: ["first_name", "last_name"], + queryParam: { key: "search", defaultValue: "" }, }); const [channels, setChannels] = useState([]); const [assignment, setAssignment] = useState(AssignedTo.ALL); @@ -46,12 +48,10 @@ export const Inbox = () => { - - onSearch("")} - className="changeColor" - onChange={(v) => onSearch(v)} + + diff --git a/frontend/src/hooks/useQueryParam.ts b/frontend/src/hooks/useQueryParam.ts new file mode 100644 index 00000000..8f1abc12 --- /dev/null +++ b/frontend/src/hooks/useQueryParam.ts @@ -0,0 +1,76 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { useRouter } from "next/router"; +import { useCallback, useMemo } from "react"; + +export type QueryParams = Record; +export type QueryParamCallback = (value: T) => void; + +export const useQueryParam = () => { + const router = useRouter(); + const { query } = router; + const queryParams: QueryParams = useMemo(() => ({ ...query }), [query]); + const updateUrl = useCallback( + async (newParams: QueryParams, defaultValue?: T) => { + const updatedQuery: QueryParams = { ...query }; + + Object.entries(newParams).forEach(([key, value]) => { + if (value === defaultValue) { + delete updatedQuery[key]; + } else { + updatedQuery[key] = value; + } + }); + + await router.replace( + { + query: updatedQuery, + }, + undefined, + { shallow: true }, + ); + }, + [query, router], + ); + const setQueryParam = useCallback( + async (key: string, value: T, defaultValue?: T) => { + return await updateUrl({ [key]: value } as QueryParams, defaultValue); + }, + [updateUrl], + ); + const removeQueryParam = useCallback( + async (key: string) => { + await updateUrl({ [key]: undefined }); + }, + [updateUrl], + ); + const getQueryParam = useCallback( + (key: T): T | undefined => { + return queryParams[key] as T; + }, + [queryParams], + ); + const clearQueryParams = useCallback(async () => { + await router.push( + { + query: {}, + }, + undefined, + { shallow: true }, + ); + }, [router]); + + return { + queryParams, + setQueryParam, + getQueryParam, + removeQueryParam, + clearQueryParams, + }; +}; diff --git a/frontend/src/hooks/useSearch.tsx b/frontend/src/hooks/useSearch.ts similarity index 55% rename from frontend/src/hooks/useSearch.tsx rename to frontend/src/hooks/useSearch.ts index ac4e2605..eed31e4c 100644 --- a/frontend/src/hooks/useSearch.tsx +++ b/frontend/src/hooks/useSearch.ts @@ -7,8 +7,7 @@ */ import { debounce } from "@mui/material"; -import { useRouter } from "next/router"; -import { ChangeEvent, useCallback, useEffect, useState } from "react"; +import { ChangeEvent, useEffect, useRef, useState } from "react"; import { TBuildInitialParamProps, @@ -16,12 +15,14 @@ import { TParamItem, } from "@/types/search.types"; -const buildOrParams = ({ params, searchText }: TBuildParamProps) => ({ +import { useQueryParam } from "./useQueryParam"; + +const buildOrParams = ({ params, searchText }: TBuildParamProps) => ({ or: params?.map((field) => ({ [field]: { contains: searchText }, })), }); -const buildILikeParams = ({ params, searchText }: TBuildParamProps) => +const buildILikeParams = ({ params, searchText }: TBuildParamProps) => params?.reduce( (acc, field) => ({ ...acc, @@ -29,7 +30,7 @@ const buildILikeParams = ({ params, searchText }: TBuildParamProps) => }), {}, ); -const buildEqInitialParams = ({ +const buildEqInitialParams = ({ initialParams, }: TBuildInitialParamProps) => initialParams?.reduce( @@ -39,7 +40,7 @@ const buildEqInitialParams = ({ }), {}, ); -const buildNeqInitialParams = ({ +const buildNeqInitialParams = ({ initialParams, }: TBuildInitialParamProps) => initialParams?.reduce( @@ -52,49 +53,50 @@ const buildNeqInitialParams = ({ {}, ); -export const useSearch = (params: TParamItem) => { - const router = useRouter(); +export const useSearch = ({ + $eq: eqInitialParams, + $neq: neqInitialParams, + $iLike: iLikeParams, + $or: orParams, + queryParam, +}: TParamItem) => { + const ref = useRef(null); + const queryParamKey = queryParam?.key || ""; + const { setQueryParam, getQueryParam } = useQueryParam(); + const queryParamValue = getQueryParam(queryParamKey); const [searchText, setSearchText] = useState( - (router.query.search as string) || "", + queryParamValue?.toString() || "", ); useEffect(() => { - if (router.query.search !== searchText) { - setSearchText((router.query.search as string) || ""); + if (searchText && ref.current) { + ref.current.value = searchText; } - }, [router.query.search]); + }, [searchText]); - const updateQueryParams = useCallback( - debounce(async (newSearchText: string) => { - await router.replace( - { - pathname: router.pathname, - query: { ...router.query, search: newSearchText || undefined }, - }, - undefined, - { shallow: true }, - ); - }, 300), - [router], + useEffect(() => { + if (queryParamKey && queryParamValue !== searchText) { + setSearchText(queryParamValue || ""); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [queryParamValue]); + + const onSearch = debounce( + async (e: ChangeEvent | string) => { + const newSearchText = typeof e === "string" ? e : e.target.value; + + setSearchText(newSearchText); + if (queryParamKey) { + setQueryParam(queryParamKey, newSearchText, queryParam?.defaultValue); + } + }, + 300, ); - const onSearch = ( - e: ChangeEvent | string, - ) => { - const newSearchText = typeof e === "string" ? e : e.target.value; - - setSearchText(newSearchText); - updateQueryParams(newSearchText); - }; - const { - $eq: eqInitialParams, - $iLike: iLikeParams, - $neq: neqInitialParams, - $or: orParams, - } = params; return { - searchText, + ref, onSearch, + searchText, searchPayload: { where: { ...buildEqInitialParams({ initialParams: eqInitialParams }), diff --git a/frontend/src/types/search.types.ts b/frontend/src/types/search.types.ts index b5dac533..79d1ad57 100644 --- a/frontend/src/types/search.types.ts +++ b/frontend/src/types/search.types.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. @@ -12,9 +12,10 @@ export type TFilterStringFields = { export type TParamItem = { $eq?: { [key in keyof T]?: T[key] }[]; - $iLike?: TFilterStringFields[]; $neq?: { [key in keyof T]?: T[key] }[]; + $iLike?: TFilterStringFields[]; $or?: TFilterStringFields[]; + queryParam?: { key: string; defaultValue?: string }; }; export type TBuildParamProps = {