fix(frontend): resolve useSearch hook flickering

This commit is contained in:
yassinedorbozgithub 2025-05-26 16:26:54 +01:00
parent 933daaa221
commit bcca90b979
4 changed files with 128 additions and 49 deletions

View File

@ -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<ISubscriber>({
const { ref, onSearch, searchPayload } = useSearch<ISubscriber>({
$or: ["first_name", "last_name"],
queryParam: { key: "search", defaultValue: "" },
});
const [channels, setChannels] = useState<string[]>([]);
const [assignment, setAssignment] = useState<AssignedTo>(AssignedTo.ALL);
@ -46,12 +48,10 @@ export const Inbox = () => {
<Grid item width="100%" height="100%" overflow="hidden">
<MainContainer style={{ height: "100%" }}>
<Sidebar position="left">
<Grid paddingX={1} paddingTop={1}>
<Search
value={searchText}
onClearClick={() => onSearch("")}
className="changeColor"
onChange={(v) => onSearch(v)}
<Grid paddingX={1} pt={2} pb={1} mx={1}>
<FilterTextfield
inputRef={ref}
onChange={onSearch}
placeholder="Search..."
/>
</Grid>

View File

@ -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<string, string | string[] | undefined>;
export type QueryParamCallback<T> = (value: T) => void;
export const useQueryParam = () => {
const router = useRouter();
const { query } = router;
const queryParams: QueryParams = useMemo(() => ({ ...query }), [query]);
const updateUrl = useCallback(
async <T>(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 <T>(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(
<T extends keyof QueryParams>(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,
};
};

View File

@ -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 = <T,>({ params, searchText }: TBuildParamProps<T>) => ({
import { useQueryParam } from "./useQueryParam";
const buildOrParams = <T>({ params, searchText }: TBuildParamProps<T>) => ({
or: params?.map((field) => ({
[field]: { contains: searchText },
})),
});
const buildILikeParams = <T,>({ params, searchText }: TBuildParamProps<T>) =>
const buildILikeParams = <T>({ params, searchText }: TBuildParamProps<T>) =>
params?.reduce(
(acc, field) => ({
...acc,
@ -29,7 +30,7 @@ const buildILikeParams = <T,>({ params, searchText }: TBuildParamProps<T>) =>
}),
{},
);
const buildEqInitialParams = <T,>({
const buildEqInitialParams = <T>({
initialParams,
}: TBuildInitialParamProps<T>) =>
initialParams?.reduce(
@ -39,7 +40,7 @@ const buildEqInitialParams = <T,>({
}),
{},
);
const buildNeqInitialParams = <T,>({
const buildNeqInitialParams = <T>({
initialParams,
}: TBuildInitialParamProps<T>) =>
initialParams?.reduce(
@ -52,49 +53,50 @@ const buildNeqInitialParams = <T,>({
{},
);
export const useSearch = <T,>(params: TParamItem<T>) => {
const router = useRouter();
export const useSearch = <T>({
$eq: eqInitialParams,
$neq: neqInitialParams,
$iLike: iLikeParams,
$or: orParams,
queryParam,
}: TParamItem<T>) => {
const ref = useRef<HTMLInputElement | null>(null);
const queryParamKey = queryParam?.key || "";
const { setQueryParam, getQueryParam } = useQueryParam();
const queryParamValue = getQueryParam(queryParamKey);
const [searchText, setSearchText] = useState<string>(
(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<HTMLInputElement | HTMLTextAreaElement> | string) => {
const newSearchText = typeof e === "string" ? e : e.target.value;
setSearchText(newSearchText);
if (queryParamKey) {
setQueryParam(queryParamKey, newSearchText, queryParam?.defaultValue);
}
},
300,
);
const onSearch = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | 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 }),

View File

@ -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<T> = {
export type TParamItem<T> = {
$eq?: { [key in keyof T]?: T[key] }[];
$iLike?: TFilterStringFields<T>[];
$neq?: { [key in keyof T]?: T[key] }[];
$iLike?: TFilterStringFields<T>[];
$or?: TFilterStringFields<T>[];
queryParam?: { key: string; defaultValue?: string };
};
export type TBuildParamProps<T> = {