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).
|
* 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 "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||||
import { Grid, MenuItem } from "@mui/material";
|
import { Grid, MenuItem } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||||
|
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||||
import { Input } from "@/app-components/inputs/Input";
|
import { Input } from "@/app-components/inputs/Input";
|
||||||
import { useSearch } from "@/hooks/useSearch";
|
import { useSearch } from "@/hooks/useSearch";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
@ -26,8 +27,9 @@ import { AssignedTo } from "./types";
|
|||||||
|
|
||||||
export const Inbox = () => {
|
export const Inbox = () => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>({
|
const { ref, onSearch, searchPayload } = useSearch<ISubscriber>({
|
||||||
$or: ["first_name", "last_name"],
|
$or: ["first_name", "last_name"],
|
||||||
|
queryParam: { key: "search", defaultValue: "" },
|
||||||
});
|
});
|
||||||
const [channels, setChannels] = useState<string[]>([]);
|
const [channels, setChannels] = useState<string[]>([]);
|
||||||
const [assignment, setAssignment] = useState<AssignedTo>(AssignedTo.ALL);
|
const [assignment, setAssignment] = useState<AssignedTo>(AssignedTo.ALL);
|
||||||
@ -46,12 +48,10 @@ export const Inbox = () => {
|
|||||||
<Grid item width="100%" height="100%" overflow="hidden">
|
<Grid item width="100%" height="100%" overflow="hidden">
|
||||||
<MainContainer style={{ height: "100%" }}>
|
<MainContainer style={{ height: "100%" }}>
|
||||||
<Sidebar position="left">
|
<Sidebar position="left">
|
||||||
<Grid paddingX={1} paddingTop={1}>
|
<Grid paddingX={1} pt={2} pb={1} mx={1}>
|
||||||
<Search
|
<FilterTextfield
|
||||||
value={searchText}
|
inputRef={ref}
|
||||||
onClearClick={() => onSearch("")}
|
onChange={onSearch}
|
||||||
className="changeColor"
|
|
||||||
onChange={(v) => onSearch(v)}
|
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</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 { debounce } from "@mui/material";
|
||||||
import { useRouter } from "next/router";
|
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||||
import { ChangeEvent, useCallback, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TBuildInitialParamProps,
|
TBuildInitialParamProps,
|
||||||
@ -16,12 +15,14 @@ import {
|
|||||||
TParamItem,
|
TParamItem,
|
||||||
} from "@/types/search.types";
|
} 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) => ({
|
or: params?.map((field) => ({
|
||||||
[field]: { contains: searchText },
|
[field]: { contains: searchText },
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
const buildILikeParams = <T,>({ params, searchText }: TBuildParamProps<T>) =>
|
const buildILikeParams = <T>({ params, searchText }: TBuildParamProps<T>) =>
|
||||||
params?.reduce(
|
params?.reduce(
|
||||||
(acc, field) => ({
|
(acc, field) => ({
|
||||||
...acc,
|
...acc,
|
||||||
@ -29,7 +30,7 @@ const buildILikeParams = <T,>({ params, searchText }: TBuildParamProps<T>) =>
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const buildEqInitialParams = <T,>({
|
const buildEqInitialParams = <T>({
|
||||||
initialParams,
|
initialParams,
|
||||||
}: TBuildInitialParamProps<T>) =>
|
}: TBuildInitialParamProps<T>) =>
|
||||||
initialParams?.reduce(
|
initialParams?.reduce(
|
||||||
@ -39,7 +40,7 @@ const buildEqInitialParams = <T,>({
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const buildNeqInitialParams = <T,>({
|
const buildNeqInitialParams = <T>({
|
||||||
initialParams,
|
initialParams,
|
||||||
}: TBuildInitialParamProps<T>) =>
|
}: TBuildInitialParamProps<T>) =>
|
||||||
initialParams?.reduce(
|
initialParams?.reduce(
|
||||||
@ -52,49 +53,50 @@ const buildNeqInitialParams = <T,>({
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useSearch = <T,>(params: TParamItem<T>) => {
|
export const useSearch = <T>({
|
||||||
const router = useRouter();
|
$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>(
|
const [searchText, setSearchText] = useState<string>(
|
||||||
(router.query.search as string) || "",
|
queryParamValue?.toString() || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.query.search !== searchText) {
|
if (searchText && ref.current) {
|
||||||
setSearchText((router.query.search as string) || "");
|
ref.current.value = searchText;
|
||||||
}
|
}
|
||||||
}, [router.query.search]);
|
}, [searchText]);
|
||||||
|
|
||||||
const updateQueryParams = useCallback(
|
useEffect(() => {
|
||||||
debounce(async (newSearchText: string) => {
|
if (queryParamKey && queryParamValue !== searchText) {
|
||||||
await router.replace(
|
setSearchText(queryParamValue || "");
|
||||||
{
|
}
|
||||||
pathname: router.pathname,
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
query: { ...router.query, search: newSearchText || undefined },
|
}, [queryParamValue]);
|
||||||
},
|
|
||||||
undefined,
|
const onSearch = debounce(
|
||||||
{ shallow: true },
|
async (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string) => {
|
||||||
);
|
|
||||||
}, 300),
|
|
||||||
[router],
|
|
||||||
);
|
|
||||||
const onSearch = (
|
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string,
|
|
||||||
) => {
|
|
||||||
const newSearchText = typeof e === "string" ? e : e.target.value;
|
const newSearchText = typeof e === "string" ? e : e.target.value;
|
||||||
|
|
||||||
setSearchText(newSearchText);
|
setSearchText(newSearchText);
|
||||||
updateQueryParams(newSearchText);
|
if (queryParamKey) {
|
||||||
};
|
setQueryParam(queryParamKey, newSearchText, queryParam?.defaultValue);
|
||||||
const {
|
}
|
||||||
$eq: eqInitialParams,
|
},
|
||||||
$iLike: iLikeParams,
|
300,
|
||||||
$neq: neqInitialParams,
|
);
|
||||||
$or: orParams,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchText,
|
ref,
|
||||||
onSearch,
|
onSearch,
|
||||||
|
searchText,
|
||||||
searchPayload: {
|
searchPayload: {
|
||||||
where: {
|
where: {
|
||||||
...buildEqInitialParams({ initialParams: eqInitialParams }),
|
...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:
|
* 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.
|
* 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> = {
|
export type TParamItem<T> = {
|
||||||
$eq?: { [key in keyof T]?: T[key] }[];
|
$eq?: { [key in keyof T]?: T[key] }[];
|
||||||
$iLike?: TFilterStringFields<T>[];
|
|
||||||
$neq?: { [key in keyof T]?: T[key] }[];
|
$neq?: { [key in keyof T]?: T[key] }[];
|
||||||
|
$iLike?: TFilterStringFields<T>[];
|
||||||
$or?: TFilterStringFields<T>[];
|
$or?: TFilterStringFields<T>[];
|
||||||
|
queryParam?: { key: string; defaultValue?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TBuildParamProps<T> = {
|
export type TBuildParamProps<T> = {
|
||||||
|
Loading…
Reference in New Issue
Block a user