mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: apply across all pages
This commit is contained in:
@@ -222,7 +222,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
{
|
||||
defaultValues: { accept, onChange },
|
||||
},
|
||||
{ maxWidth: "xl" },
|
||||
{ maxWidth: "xl", isSingleton: true },
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
InputAdornment,
|
||||
TextFieldProps,
|
||||
} from "@mui/material";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
@@ -37,30 +37,49 @@ export const FilterTextfield = ({
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const [inputValue, setInputValue] = useState(defaultValue);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const isTyping = useRef(false);
|
||||
const toggleTyping = useMemo(
|
||||
() =>
|
||||
debounce((value: boolean) => {
|
||||
isTyping.current = value;
|
||||
}, delay * 2),
|
||||
[delay],
|
||||
);
|
||||
const debouncedSearch = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
onSearch?.(value);
|
||||
toggleTyping(false);
|
||||
}, delay),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[onSearch, delay],
|
||||
);
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
|
||||
setInputValue(value);
|
||||
toggleTyping(true);
|
||||
debouncedSearch(value);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[debouncedSearch],
|
||||
);
|
||||
const handleClear = useCallback(() => {
|
||||
setInputValue("");
|
||||
debouncedSearch("");
|
||||
}, [debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
// Avoid infinite loop cycle (input => URL update => default value)
|
||||
if (defaultValue !== ref.current?.value && !isTyping.current) {
|
||||
ref.current && (ref.current.value = defaultValue);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValue]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
inputRef={ref}
|
||||
InputProps={{
|
||||
startAdornment: <Adornment Icon={SearchIcon} />,
|
||||
endAdornment: clearable && (
|
||||
@@ -73,7 +92,8 @@ export const FilterTextfield = ({
|
||||
}}
|
||||
placeholder={t("placeholder.keywords")}
|
||||
{...props}
|
||||
value={inputValue}
|
||||
// value={inputValue}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -43,9 +43,12 @@ export const Categories = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ICategory>({
|
||||
$iLike: ["label"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ICategory>(
|
||||
{
|
||||
$iLike: ["label"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CATEGORY },
|
||||
{
|
||||
@@ -142,7 +145,7 @@ export const Categories = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CATEGORY, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -40,9 +40,12 @@ export const ContentTypes = () => {
|
||||
const router = useRouter();
|
||||
const dialogs = useDialogs();
|
||||
// data fetching
|
||||
const { onSearch, searchPayload } = useSearch<IContentType>({
|
||||
$iLike: ["name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IContentType>(
|
||||
{
|
||||
$iLike: ["name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CONTENT_TYPE },
|
||||
{
|
||||
@@ -99,7 +102,7 @@ export const ContentTypes = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CONTENT_TYPE, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -57,10 +57,13 @@ export const Contents = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dialogs = useDialogs();
|
||||
// data fetching
|
||||
const { onSearch, searchPayload } = useSearch<IContent>({
|
||||
$eq: [{ entity: String(query.id) }],
|
||||
$iLike: ["title"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IContent>(
|
||||
{
|
||||
$eq: [{ entity: String(query.id) }],
|
||||
$iLike: ["title"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const hasPermission = useHasPermission();
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CONTENT, format: Format.FULL },
|
||||
@@ -157,7 +160,7 @@ export const Contents = () => {
|
||||
>
|
||||
<Grid justifyContent="flex-end" gap={1} container alignItems="center">
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? (
|
||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||
|
||||
@@ -43,9 +43,12 @@ export const ContextVars = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<IContextVar>({
|
||||
$iLike: ["label"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IContextVar>(
|
||||
{
|
||||
$iLike: ["label"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CONTEXT_VAR },
|
||||
{
|
||||
@@ -176,7 +179,7 @@ export const ContextVars = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CONTEXT_VAR, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -27,9 +27,12 @@ import { AssignedTo } from "./types";
|
||||
|
||||
export const Inbox = () => {
|
||||
const { t } = useTranslate();
|
||||
const { onSearch, searchPayload } = useSearch<ISubscriber>({
|
||||
$or: ["first_name", "last_name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>(
|
||||
{
|
||||
$or: ["first_name", "last_name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const [channels, setChannels] = useState<string[]>([]);
|
||||
const [assignment, setAssignment] = useState<AssignedTo>(AssignedTo.ALL);
|
||||
|
||||
@@ -48,13 +51,10 @@ export const Inbox = () => {
|
||||
<MainContainer style={{ height: "100%" }}>
|
||||
<Sidebar position="left">
|
||||
<Grid paddingX={1} paddingTop={2} paddingBottom={1} mx={1}>
|
||||
<FilterTextfield onChange={onSearch} defaultValue="" />
|
||||
{/* <Search
|
||||
onClearClick={() => onSearch("")}
|
||||
className="changeColor"
|
||||
onChange={(v) => onSearch(v)}
|
||||
placeholder="Search..."
|
||||
/> */}
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
display="flex"
|
||||
|
||||
@@ -42,9 +42,12 @@ export const Labels = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ILabel>({
|
||||
$or: ["name", "title"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ILabel>(
|
||||
{
|
||||
$or: ["name", "title"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.LABEL, format: Format.FULL },
|
||||
{
|
||||
@@ -173,7 +176,7 @@ export const Labels = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.LABEL, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -43,9 +43,12 @@ export const Languages = () => {
|
||||
const dialogs = useDialogs();
|
||||
const queryClient = useQueryClient();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ILanguage>({
|
||||
$or: ["title", "code"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ILanguage>(
|
||||
{
|
||||
$or: ["title", "code"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps, refetch } = useFind(
|
||||
{ entity: EntityType.LANGUAGE },
|
||||
{
|
||||
@@ -197,7 +200,7 @@ export const Languages = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -37,9 +37,13 @@ type MediaLibraryProps = {
|
||||
export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||
const { t } = useTranslate();
|
||||
const formatFileSize = useFormattedFileSize();
|
||||
const { onSearch, searchPayload } = useSearch<IAttachment>({
|
||||
$iLike: ["name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IAttachment>(
|
||||
{
|
||||
$iLike: ["name"],
|
||||
},
|
||||
// Sync URL only in the media library page (not the modal)
|
||||
{ syncUrl: !onSelect },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.ATTACHMENT },
|
||||
{
|
||||
@@ -151,7 +155,7 @@ export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</PageHeader>
|
||||
|
||||
@@ -80,9 +80,12 @@ const NlpEntity = () => {
|
||||
},
|
||||
});
|
||||
const [selectedNlpEntities, setSelectedNlpEntities] = useState<string[]>([]);
|
||||
const { onSearch, searchPayload } = useSearch<INlpEntity>({
|
||||
$or: ["name", "doc"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<INlpEntity>(
|
||||
{
|
||||
$or: ["name", "doc"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps: nlpEntityGrid } = useFind(
|
||||
{
|
||||
entity: EntityType.NLP_ENTITY,
|
||||
@@ -224,7 +227,7 @@ const NlpEntity = () => {
|
||||
flexShrink={0}
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
|
||||
{hasPermission(EntityType.NLP_ENTITY, PermissionAction.CREATE) ? (
|
||||
|
||||
@@ -86,13 +86,16 @@ export default function NlpSample() {
|
||||
EntityType.NLP_SAMPLE_ENTITY,
|
||||
);
|
||||
const getLanguageFromCache = useGetFromCache(EntityType.LANGUAGE);
|
||||
const { onSearch, searchPayload } = useSearch<INlpSample>({
|
||||
$eq: [
|
||||
...(type !== "all" ? [{ type }] : []),
|
||||
...(language ? [{ language }] : []),
|
||||
],
|
||||
$iLike: ["text"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<INlpSample>(
|
||||
{
|
||||
$eq: [
|
||||
...(type !== "all" ? [{ type }] : []),
|
||||
...(language ? [{ language }] : []),
|
||||
],
|
||||
$iLike: ["text"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { mutate: deleteNlpSample } = useDelete(EntityType.NLP_SAMPLE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
@@ -317,6 +320,7 @@ export default function NlpSample() {
|
||||
>
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
defaultValue={searchText}
|
||||
fullWidth={false}
|
||||
sx={{ minWidth: "256px" }}
|
||||
/>
|
||||
|
||||
@@ -51,10 +51,13 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
entity: EntityType.NLP_ENTITY,
|
||||
format: Format.FULL,
|
||||
});
|
||||
const { onSearch, searchPayload } = useSearch<INlpValue>({
|
||||
$eq: [{ entity: entityId }],
|
||||
$or: ["doc", "value"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<INlpValue>(
|
||||
{
|
||||
$eq: [{ entity: entityId }],
|
||||
$or: ["doc", "value"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.NLP_VALUE, format: Format.FULL },
|
||||
{
|
||||
@@ -228,7 +231,10 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
sx={{ width: "max-content", gap: 1 }}
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Grid>
|
||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||
{hasPermission(
|
||||
|
||||
@@ -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.
|
||||
@@ -40,9 +40,12 @@ export const Roles = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<IRole>({
|
||||
$iLike: ["name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IRole>(
|
||||
{
|
||||
$iLike: ["name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.ROLE },
|
||||
{
|
||||
@@ -140,7 +143,7 @@ export const Roles = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.ROLE, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -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.
|
||||
@@ -43,10 +43,13 @@ export const Subscribers = () => {
|
||||
{ hasCount: false },
|
||||
);
|
||||
const [labelFilter, setLabelFilter] = useState<string>("");
|
||||
const { onSearch, searchPayload } = useSearch<ISubscriber>({
|
||||
$eq: labelFilter ? [{ labels: [labelFilter] }] : [],
|
||||
$or: ["first_name", "last_name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>(
|
||||
{
|
||||
$eq: labelFilter ? [{ labels: [labelFilter] }] : [],
|
||||
$or: ["first_name", "last_name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.SUBSCRIBER, format: Format.FULL },
|
||||
{ params: searchPayload },
|
||||
@@ -172,7 +175,11 @@ export const Subscribers = () => {
|
||||
flexWrap="nowrap"
|
||||
width="50%"
|
||||
>
|
||||
<FilterTextfield onChange={onSearch} fullWidth={true} />
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
fullWidth={true}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
<Input
|
||||
select
|
||||
label={t("label.labels")}
|
||||
|
||||
@@ -44,9 +44,12 @@ export const Translations = () => {
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const { onSearch, searchPayload } = useSearch<ITranslation>({
|
||||
$iLike: ["str"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ITranslation>(
|
||||
{
|
||||
$iLike: ["str"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps, refetch: refreshTranslations } = useFind(
|
||||
{ entity: EntityType.TRANSLATION },
|
||||
{
|
||||
@@ -152,7 +155,7 @@ export const Translations = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
|
||||
@@ -53,9 +53,12 @@ export const Users = () => {
|
||||
},
|
||||
});
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<IUser>({
|
||||
$or: ["first_name", "last_name", "email"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IUser>(
|
||||
{
|
||||
$or: ["first_name", "last_name", "email"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { data: roles } = useFind(
|
||||
{
|
||||
entity: EntityType.ROLE,
|
||||
@@ -102,7 +105,7 @@ export const Users = () => {
|
||||
headerName: t("label.name"),
|
||||
sortable: false,
|
||||
disableColumnMenu: true,
|
||||
valueGetter: (params, val) => `${val.first_name} ${val.last_name}`,
|
||||
valueGetter: (_params, val) => `${val.first_name} ${val.last_name}`,
|
||||
headerAlign: "left",
|
||||
renderHeader,
|
||||
},
|
||||
@@ -198,7 +201,7 @@ export const Users = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{!ssoEnabled &&
|
||||
hasPermission(EntityType.USER, PermissionAction.CREATE) ? (
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
TParamItem,
|
||||
} from "@/types/search.types";
|
||||
|
||||
import { useUrlQueryParam } from "./useUrlQueryParam";
|
||||
|
||||
const buildOrParams = <T,>({ params, searchText }: TBuildParamProps<T>) => ({
|
||||
or: params?.map((field) => ({
|
||||
[field]: { contains: searchText },
|
||||
@@ -50,20 +52,38 @@ const buildNeqInitialParams = <T,>({
|
||||
{},
|
||||
);
|
||||
|
||||
export const useSearch = <T,>(params: TParamItem<T>) => {
|
||||
const [searchText, setSearchText] = useState<string>("");
|
||||
interface SearchHookOptions {
|
||||
syncUrl?: boolean;
|
||||
}
|
||||
|
||||
export const useSearch = <T,>(
|
||||
params: TParamItem<T>,
|
||||
options: SearchHookOptions = { syncUrl: false },
|
||||
) => {
|
||||
const { syncUrl } = options;
|
||||
const [searchQuery, setSearchQuery] = useUrlQueryParam("search", "");
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const {
|
||||
$eq: eqInitialParams,
|
||||
$iLike: iLikeParams,
|
||||
$neq: neqInitialParams,
|
||||
$or: orParams,
|
||||
} = params;
|
||||
const searchText = syncUrl ? searchQuery : search;
|
||||
|
||||
return {
|
||||
searchText,
|
||||
onSearch: (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string,
|
||||
) => {
|
||||
setSearchText(typeof e === "string" ? e : e.target.value);
|
||||
const newValue =
|
||||
typeof e === "object" ? e.target.value.toString() : e.toString();
|
||||
|
||||
if (syncUrl) {
|
||||
setSearchQuery(newValue);
|
||||
} else {
|
||||
setSearch(newValue);
|
||||
}
|
||||
},
|
||||
searchPayload: {
|
||||
where: {
|
||||
|
||||
@@ -9,9 +9,48 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
type QueryParamSerializer<T> = {
|
||||
parse: (raw: string | string[] | undefined) => T;
|
||||
stringify: (val: T) => string | string[] | undefined;
|
||||
};
|
||||
|
||||
export function defaultSerializer<T = string>(): QueryParamSerializer<T> {
|
||||
return {
|
||||
parse: (raw) => {
|
||||
if (Array.isArray(raw)) return raw[0] as unknown as T;
|
||||
if (typeof raw === "undefined") return "" as unknown as T;
|
||||
|
||||
return raw as unknown as T;
|
||||
},
|
||||
stringify: (val) => val as unknown as string,
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanSerializer(): QueryParamSerializer<boolean> {
|
||||
return {
|
||||
parse: (raw) => raw === "true",
|
||||
stringify: (val) => (val ? "true" : "false"),
|
||||
};
|
||||
}
|
||||
|
||||
export function numberSerializer(): QueryParamSerializer<number> {
|
||||
return {
|
||||
parse: (raw) => Number(Array.isArray(raw) ? raw[0] : raw),
|
||||
stringify: (val) => val.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function arraySerializer(): QueryParamSerializer<string[]> {
|
||||
return {
|
||||
parse: (raw) => (Array.isArray(raw) ? raw : raw ? [raw] : []),
|
||||
stringify: (val) => val,
|
||||
};
|
||||
}
|
||||
|
||||
export const useUrlQueryParam = <T>(
|
||||
key: string,
|
||||
defaultValue: T
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
serializer: QueryParamSerializer<T> = defaultSerializer(),
|
||||
): [T, (val: T) => void] => {
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
@@ -21,7 +60,7 @@ export const useUrlQueryParam = <T>(
|
||||
if (initial === undefined) return defaultValue;
|
||||
// parse value if needed (e.g., numbers)
|
||||
try {
|
||||
return JSON.parse(initial as string) as T;
|
||||
return serializer.parse(initial as string) as T;
|
||||
} catch {
|
||||
return initial as unknown as T;
|
||||
}
|
||||
@@ -35,7 +74,7 @@ export const useUrlQueryParam = <T>(
|
||||
|
||||
if (urlValue !== undefined) {
|
||||
try {
|
||||
parsedVal = JSON.parse(urlValue as string);
|
||||
parsedVal = serializer.parse(urlValue as string);
|
||||
} catch {
|
||||
parsedVal = urlValue as unknown as T;
|
||||
}
|
||||
@@ -49,19 +88,24 @@ export const useUrlQueryParam = <T>(
|
||||
}, [router.isReady, router.query[key]]);
|
||||
|
||||
// Update URL when state changes
|
||||
const updateValue = useCallback((val: T) => {
|
||||
debugger
|
||||
setValue(val);
|
||||
if (!router.isReady) return;
|
||||
const newQuery = { ...router.query };
|
||||
const updateValue = useCallback(
|
||||
(val: T) => {
|
||||
setValue(val);
|
||||
if (!router.isReady) return;
|
||||
const newQuery = { ...router.query };
|
||||
|
||||
if (val === defaultValue || val === undefined || val === '') {
|
||||
delete newQuery[key];
|
||||
} else {
|
||||
newQuery[key] = typeof val === 'string' ? val : JSON.stringify(val);
|
||||
}
|
||||
router.push({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });
|
||||
}, [router, key, defaultValue]);
|
||||
if (val === defaultValue || val === undefined || val === "") {
|
||||
delete newQuery[key];
|
||||
} else {
|
||||
newQuery[key] = serializer.stringify(val);
|
||||
}
|
||||
router.push({ pathname: router.pathname, query: newQuery }, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[router, key, defaultValue],
|
||||
);
|
||||
|
||||
return [value, updateValue];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user