fix: add draft useUrlQuery and useQueryParam hooks

This commit is contained in:
yassinedorbozgithub 2025-05-09 14:57:55 +01:00
parent 0db40680dc
commit bc1d75af88
5 changed files with 176 additions and 48 deletions

View File

@ -43,7 +43,7 @@ export const Categories = () => {
const { toast } = useToast();
const dialogs = useDialogs();
const hasPermission = useHasPermission();
const { onSearch, searchPayload } = useSearch<ICategory>({
const { searchPayload, textFieldProps } = useSearch<ICategory>({
$iLike: ["label"],
});
const { dataGridProps } = useFind(
@ -142,7 +142,7 @@ export const Categories = () => {
width="max-content"
>
<Grid item>
<FilterTextfield onChange={onSearch} />
<FilterTextfield {...textFieldProps} />
</Grid>
{hasPermission(EntityType.CATEGORY, PermissionAction.CREATE) ? (
<Grid item>

View File

@ -6,13 +6,15 @@
* 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 { useQueryParam } from "@/hooks/useQueryParam";
import { useSearch } from "@/hooks/useSearch";
import { useTranslate } from "@/hooks/useTranslate";
import { EntityType, Format } from "@/services/types";
@ -26,11 +28,16 @@ import { AssignedTo } from "./types";
export const Inbox = () => {
const { t } = useTranslate();
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>({
const [assignment, setAssignment] = useState<AssignedTo>(AssignedTo.ALL);
const { searchPayload, textFieldProps } = useSearch<ISubscriber>({
$or: ["first_name", "last_name"],
});
const [channels, setChannels] = useState<string[]>([]);
const [assignment, setAssignment] = useState<AssignedTo>(AssignedTo.ALL);
useQueryParam("assigned_to", assignment, AssignedTo.ALL, setAssignment);
useQueryParam("channel", channels, [], () => {
// setChannels((channels) => [...channels, channel] as any);
});
return (
<ChatProvider>
@ -46,14 +53,8 @@ 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)}
placeholder="Search..."
/>
<Grid paddingX={1} pt={2} pb={1} mx={1}>
<FilterTextfield placeholder="Search..." {...textFieldProps} />
</Grid>
<Grid
display="flex"
@ -80,7 +81,7 @@ export const Inbox = () => {
onChange={(e) => setAssignment(e.target.value as AssignedTo)}
label={t("label.assigned_to")}
select
defaultValue={AssignedTo.ALL}
value={assignment}
sx={{ marginTop: 1 }}
>
<MenuItem value={AssignedTo.ALL}>All</MenuItem>

View File

@ -0,0 +1,50 @@
/*
* 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 { useEffect } from "react";
import { QueryParamCallback, useUrlQuery } from "./useUrlQuery";
export const useQueryParam = <
T extends string | string[] | undefined = string | undefined,
>(
key: string,
value: T,
defaultValue: T,
callback?: QueryParamCallback<T>,
) => {
const router = useRouter();
const { queryParams, setQueryParam } = useUrlQuery();
useEffect(() => {
if (router.isReady) {
setQueryParam(key, value, defaultValue);
}
}, [value, router.isReady]);
useEffect(() => {
if (callback && router.isReady) {
if (Array.isArray(defaultValue)) {
if (typeof queryParams[key] === "string") {
callback([queryParams[key]] as T);
} else {
if (value?.length === 0) {
} else if (
value?.length &&
value?.length !== queryParams[key]?.length
) {
callback((queryParams[key] || defaultValue) as T);
}
}
} else {
callback((queryParams[key] || defaultValue) as T);
}
}
}, [key, queryParams[key], router.isReady]);
};

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, useState } from "react";
import {
TBuildInitialParamProps,
@ -16,6 +15,9 @@ import {
TParamItem,
} from "@/types/search.types";
import { useQueryParam } from "./useQueryParam";
import { useUrlQuery } from "./useUrlQuery";
const buildOrParams = <T,>({ params, searchText }: TBuildParamProps<T>) => ({
or: params?.map((field) => ({
[field]: { contains: searchText },
@ -53,48 +55,29 @@ const buildNeqInitialParams = <T,>({
);
export const useSearch = <T,>(params: TParamItem<T>) => {
const router = useRouter();
const { getQueryParam } = useUrlQuery();
const [searchText, setSearchText] = useState<string>(
(router.query.search as string) || "",
!!getQueryParam("search") ? String(getQueryParam("search")) : "",
);
useEffect(() => {
if (router.query.search !== searchText) {
setSearchText((router.query.search as string) || "");
}
}, [router.query.search]);
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,
) => {
const newSearchText = typeof e === "string" ? e : e.target.value;
setSearchText(newSearchText);
updateQueryParams(newSearchText);
};
const [isActive, setIsActive] = useState(false);
const {
$eq: eqInitialParams,
$iLike: iLikeParams,
$neq: neqInitialParams,
$or: orParams,
$iLike: iLikeParams,
} = params;
const onSearch = debounce(
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string) => {
setSearchText(typeof e === "string" ? e : e.target.value);
},
300,
);
useQueryParam("search", searchText, "", setSearchText);
return {
searchText,
onSearch,
searchText,
searchPayload: {
where: {
...buildEqInitialParams({ initialParams: eqInitialParams }),
@ -105,5 +88,15 @@ export const useSearch = <T,>(params: TParamItem<T>) => {
}),
},
},
textFieldProps: {
value: isActive ? undefined : searchText,
onChange: onSearch,
onMouseOver: () => {
setIsActive(true);
},
onMouseLeave: () => {
setIsActive(false);
},
},
};
};

View File

@ -0,0 +1,84 @@
/*
* 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 useUrlQuery = () => {
const router = useRouter();
const { pathname, query } = router;
const queryParams: QueryParams = useMemo(() => {
return { ...query };
}, [query]);
const updateUrl = useCallback(
async (newParams: QueryParams, defaultValue?: string | string[]) => {
const updatedQuery: QueryParams = { ...query };
Object.entries(newParams).forEach(([key, value]) => {
if (value === defaultValue) {
// delete updatedQuery[key];
} else {
updatedQuery[key] = value;
}
});
await router.push(
{
pathname,
query: updatedQuery,
},
undefined,
{ shallow: true },
);
},
[pathname, query, router],
);
const setQueryParam = useCallback(
async (
key: string,
value: string | string[] | undefined,
defaultValue?: string | string[],
) => {
await updateUrl({ [key]: value }, defaultValue);
},
[updateUrl],
);
const removeQueryParam = useCallback(
(key: string) => {
updateUrl({ [key]: undefined });
},
[updateUrl],
);
const getQueryParam = useCallback(
(key: string): string | string[] | undefined => {
return queryParams[key];
},
[queryParams],
);
const clearQueryParams = useCallback(async () => {
await router.push(
{
pathname,
query: {},
},
undefined,
{ shallow: true },
);
}, [pathname, router]);
return {
queryParams,
setQueryParam,
getQueryParam,
removeQueryParam,
clearQueryParams,
};
};