mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix(frontend): resolve useSearch hook flickering
This commit is contained in:
parent
933daaa221
commit
bcca90b979
@ -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>
|
||||
|
76
frontend/src/hooks/useQueryParam.ts
Normal file
76
frontend/src/hooks/useQueryParam.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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],
|
||||
);
|
||||
const onSearch = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string,
|
||||
) => {
|
||||
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);
|
||||
updateQueryParams(newSearchText);
|
||||
};
|
||||
const {
|
||||
$eq: eqInitialParams,
|
||||
$iLike: iLikeParams,
|
||||
$neq: neqInitialParams,
|
||||
$or: orParams,
|
||||
} = params;
|
||||
if (queryParamKey) {
|
||||
setQueryParam(queryParamKey, newSearchText, queryParam?.defaultValue);
|
||||
}
|
||||
},
|
||||
300,
|
||||
);
|
||||
|
||||
return {
|
||||
searchText,
|
||||
ref,
|
||||
onSearch,
|
||||
searchText,
|
||||
searchPayload: {
|
||||
where: {
|
||||
...buildEqInitialParams({ initialParams: eqInitialParams }),
|
@ -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> = {
|
||||
|
Loading…
Reference in New Issue
Block a user