mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge pull request #1052 from Hexastack/fix/use-search-v3
Fix/use search v3
This commit is contained in:
commit
74a12e1559
@ -222,7 +222,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
{
|
||||
defaultValues: { accept, onChange },
|
||||
},
|
||||
{ maxWidth: "xl" },
|
||||
{ maxWidth: "xl", isSingleton: true },
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -1,31 +1,100 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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 ClearIcon from "@mui/icons-material/Clear";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { TextFieldProps } from "@mui/material";
|
||||
import {
|
||||
debounce,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextFieldProps,
|
||||
} from "@mui/material";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
import { Adornment } from "./Adornment";
|
||||
import { Input } from "./Input";
|
||||
|
||||
export const FilterTextfield = (props: TextFieldProps) => {
|
||||
const { t } = useTranslate();
|
||||
export interface FilterTextFieldProps
|
||||
extends Omit<TextFieldProps, "value" | "onChange"> {
|
||||
onChange: (value: string) => void;
|
||||
delay?: number;
|
||||
clearable: boolean;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export const FilterTextfield = ({
|
||||
onChange: onSearch,
|
||||
defaultValue = "",
|
||||
delay = 500,
|
||||
clearable = true,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
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;
|
||||
|
||||
toggleTyping(true);
|
||||
debouncedSearch(value);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[debouncedSearch],
|
||||
);
|
||||
const handleClear = useCallback(() => {
|
||||
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]);
|
||||
|
||||
//TODO: replace the native delete text button by a styled custom button
|
||||
return (
|
||||
<Input
|
||||
type="search"
|
||||
inputRef={ref}
|
||||
InputProps={{
|
||||
startAdornment: <Adornment Icon={SearchIcon} />,
|
||||
endAdornment: clearable && (
|
||||
<InputAdornment position="end" onClick={handleClear}>
|
||||
<IconButton size="small" sx={{ marginRight: -1 }}>
|
||||
<ClearIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
placeholder={t("placeholder.keywords")}
|
||||
{...props}
|
||||
// value={inputValue}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,59 +1,40 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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 React, { FC, SVGProps } from "react";
|
||||
import { FC, SVGProps } from "react";
|
||||
|
||||
const NoDataIcon: FC<SVGProps<SVGSVGElement>> = ({ ...props }) => {
|
||||
const NoDataIcon: FC<SVGProps<SVGSVGElement>> = ({ width = 96, ...props }) => {
|
||||
return (
|
||||
<svg
|
||||
height="150"
|
||||
viewBox="0 0 184 152"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
width={width}
|
||||
viewBox="0 0 452 257"
|
||||
aria-hidden
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
<title>No data</title>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<g transform="translate(24 31.67)">
|
||||
<ellipse
|
||||
fillOpacity=".8"
|
||||
fill="#F5F5F7"
|
||||
cx="67.797"
|
||||
cy="106.89"
|
||||
rx="67.797"
|
||||
ry="12.668"
|
||||
<path
|
||||
className="no-rows-primary"
|
||||
d="M348 69c-46.392 0-84 37.608-84 84s37.608 84 84 84 84-37.608 84-84-37.608-84-84-84Zm-104 84c0-57.438 46.562-104 104-104s104 46.562 104 104-46.562 104-104 104-104-46.562-104-104Z"
|
||||
/>
|
||||
<path
|
||||
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
|
||||
fill="#AEB8C2"
|
||||
className="no-rows-primary"
|
||||
d="M308.929 113.929c3.905-3.905 10.237-3.905 14.142 0l63.64 63.64c3.905 3.905 3.905 10.236 0 14.142-3.906 3.905-10.237 3.905-14.142 0l-63.64-63.64c-3.905-3.905-3.905-10.237 0-14.142Z"
|
||||
/>
|
||||
<path
|
||||
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
|
||||
fill="url(#linearGradient-1)"
|
||||
transform="translate(13.56)"
|
||||
className="no-rows-primary"
|
||||
d="M308.929 191.711c-3.905-3.906-3.905-10.237 0-14.142l63.64-63.64c3.905-3.905 10.236-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142l-63.64 63.64c-3.905 3.905-10.237 3.905-14.142 0Z"
|
||||
/>
|
||||
<path
|
||||
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
|
||||
fill="#F5F5F7"
|
||||
className="no-rows-secondary"
|
||||
d="M0 10C0 4.477 4.477 0 10 0h380c5.523 0 10 4.477 10 10s-4.477 10-10 10H10C4.477 20 0 15.523 0 10ZM0 59c0-5.523 4.477-10 10-10h231c5.523 0 10 4.477 10 10s-4.477 10-10 10H10C4.477 69 0 64.523 0 59ZM0 106c0-5.523 4.477-10 10-10h203c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10ZM0 153c0-5.523 4.477-10 10-10h195.5c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10ZM0 200c0-5.523 4.477-10 10-10h203c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10ZM0 247c0-5.523 4.477-10 10-10h231c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10Z"
|
||||
/>
|
||||
<path
|
||||
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
|
||||
fill="#DCE0E6"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
|
||||
fill="#DCE0E6"
|
||||
/>
|
||||
<g transform="translate(149.65 15.383)" fill="#FFF">
|
||||
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815" />
|
||||
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@ -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.
|
||||
@ -73,6 +73,12 @@ export const DataGrid = <T extends GridValidRowModel = any>({
|
||||
autoHeight={autoHeight}
|
||||
disableRowSelectionOnClick={disableRowSelectionOnClick}
|
||||
slots={slots}
|
||||
slotProps={{
|
||||
loadingOverlay: {
|
||||
variant: "linear-progress",
|
||||
noRowsVariant: "skeleton",
|
||||
},
|
||||
}}
|
||||
showCellVerticalBorder={showCellVerticalBorder}
|
||||
showColumnVerticalBorder={showColumnVerticalBorder}
|
||||
sx={sx}
|
||||
|
@ -1,42 +1,46 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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 { Grid, Typography } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
import NoDataIcon from "../svg/NoDataIcon";
|
||||
|
||||
const StyledGridOverlay = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
minHeight: "200px",
|
||||
"& .no-rows-primary": {
|
||||
fill: "#3D4751",
|
||||
...theme.applyStyles("light", {
|
||||
fill: "#AEB8C2",
|
||||
}),
|
||||
},
|
||||
"& .no-rows-secondary": {
|
||||
fill: "#1D2126",
|
||||
...theme.applyStyles("light", {
|
||||
fill: "#E8EAED",
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
export const NoDataOverlay = () => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
height: "fit-content",
|
||||
gap: 1,
|
||||
opacity: 0.5,
|
||||
paddingY: 1,
|
||||
}}
|
||||
>
|
||||
<StyledGridOverlay>
|
||||
<NoDataIcon />
|
||||
<Grid item>
|
||||
<Typography
|
||||
style={{
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
{t("label.no_data")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box sx={{ mt: 2 }}>{t("label.no_data")}</Box>
|
||||
</StyledGridOverlay>
|
||||
);
|
||||
};
|
||||
|
@ -43,9 +43,12 @@ export const Categories = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ICategory>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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>
|
||||
|
@ -21,7 +21,6 @@ import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { Title } from "@/layout/content/Title";
|
||||
import { EntityType, RouterType } from "@/services/types";
|
||||
import { normalizeDate } from "@/utils/date";
|
||||
import { extractQueryParamsUrl } from "@/utils/URL";
|
||||
|
||||
import { getAvatarSrc } from "../helpers/mapMessages";
|
||||
import { useChat } from "../hooks/ChatContext";
|
||||
@ -33,8 +32,8 @@ export const SubscribersList = (props: {
|
||||
searchPayload: any;
|
||||
assignedTo: AssignedTo;
|
||||
}) => {
|
||||
const { query, push } = useRouter();
|
||||
const subscriber = query.subscriber?.toString() || null;
|
||||
const router = useRouter();
|
||||
const subscriber = router.query.subscriber?.toString() || null;
|
||||
const { apiUrl } = useConfig();
|
||||
const { t, i18n } = useTranslate();
|
||||
const chat = useChat();
|
||||
@ -75,10 +74,17 @@ export const SubscribersList = (props: {
|
||||
<Conversation
|
||||
onClick={() => {
|
||||
chat.setSubscriberId(subscriber.id);
|
||||
push({
|
||||
pathname: `/${RouterType.INBOX}/subscribers/${subscriber.id}`,
|
||||
query: extractQueryParamsUrl(window.location.href),
|
||||
});
|
||||
router.push(
|
||||
{
|
||||
pathname: `/${RouterType.INBOX}/subscribers/[subscriber]`,
|
||||
query: {
|
||||
...router.query,
|
||||
subscriber: subscriber.id,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}}
|
||||
className="changeColor"
|
||||
key={subscriber.id}
|
||||
|
@ -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,9 +27,12 @@ import { AssignedTo } from "./types";
|
||||
|
||||
export const Inbox = () => {
|
||||
const { t } = useTranslate();
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>({
|
||||
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);
|
||||
|
||||
@ -46,13 +50,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)}
|
||||
placeholder="Search..."
|
||||
<Grid paddingX={1} paddingTop={2} paddingBottom={1} mx={1}>
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
|
@ -42,9 +42,12 @@ export const Labels = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ILabel>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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>({
|
||||
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) ? (
|
||||
|
@ -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.
|
||||
@ -86,6 +86,7 @@ export const useFind = <
|
||||
);
|
||||
}
|
||||
},
|
||||
keepPreviousData: true,
|
||||
...otherOptions,
|
||||
});
|
||||
const data = (ids || [])
|
||||
@ -99,7 +100,7 @@ export const useFind = <
|
||||
dataGridProps: {
|
||||
...dataGridPaginationProps,
|
||||
rows: data || [],
|
||||
loading: normalizedQuery.isLoading,
|
||||
loading: normalizedQuery.isLoading || normalizedQuery.isFetching,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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.
|
||||
@ -74,6 +74,7 @@ export const useInfiniteFind = <
|
||||
|
||||
return result;
|
||||
},
|
||||
keepPreviousData: true,
|
||||
...(otherOptions || {}),
|
||||
});
|
||||
|
||||
|
@ -6,9 +6,7 @@
|
||||
* 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 { 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 +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 },
|
||||
@ -52,49 +52,39 @@ const buildNeqInitialParams = <T,>({
|
||||
{},
|
||||
);
|
||||
|
||||
export const useSearch = <T,>(params: TParamItem<T>) => {
|
||||
const router = useRouter();
|
||||
const [searchText, setSearchText] = useState<string>(
|
||||
(router.query.search as string) || "",
|
||||
);
|
||||
interface SearchHookOptions {
|
||||
syncUrl?: boolean;
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
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,
|
||||
onSearch: (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string,
|
||||
) => {
|
||||
const newValue =
|
||||
typeof e === "object" ? e.target.value.toString() : e.toString();
|
||||
|
||||
if (syncUrl) {
|
||||
setSearchQuery(newValue);
|
||||
} else {
|
||||
setSearch(newValue);
|
||||
}
|
||||
},
|
||||
searchPayload: {
|
||||
where: {
|
||||
...buildEqInitialParams({ initialParams: eqInitialParams }),
|
||||
|
119
frontend/src/hooks/useUrlQueryParam.ts
Normal file
119
frontend/src/hooks/useUrlQueryParam.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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, 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) => {
|
||||
const value = Number(Array.isArray(raw) ? raw[0] : raw);
|
||||
|
||||
return isNaN(value) ? 0 : value;
|
||||
},
|
||||
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,
|
||||
serializer: QueryParamSerializer<T> = defaultSerializer(),
|
||||
): [T, (val: T) => void] => {
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
// On initial load, use query or default
|
||||
const initial = router.query[key];
|
||||
|
||||
if (initial === undefined) return defaultValue;
|
||||
// parse value if needed (e.g., numbers)
|
||||
try {
|
||||
return serializer.parse(initial) as T;
|
||||
} catch {
|
||||
return initial as unknown as T;
|
||||
}
|
||||
});
|
||||
|
||||
// Sync from URL to state on changes (including initial load when ready)
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return;
|
||||
const urlValue = router.query[key];
|
||||
let parsedVal: T = defaultValue;
|
||||
|
||||
if (urlValue !== undefined) {
|
||||
try {
|
||||
parsedVal = serializer.parse(urlValue);
|
||||
} catch {
|
||||
parsedVal = urlValue as unknown as T;
|
||||
}
|
||||
} else {
|
||||
parsedVal = defaultValue;
|
||||
}
|
||||
if (parsedVal !== value) {
|
||||
setValue(parsedVal);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady, router.query[key]]);
|
||||
|
||||
// Update URL when state changes
|
||||
const updateValue = useCallback(
|
||||
(val: T) => {
|
||||
setValue(val);
|
||||
if (!router.isReady) return;
|
||||
const newQuery = { ...router.query };
|
||||
|
||||
if (
|
||||
val === defaultValue ||
|
||||
val === undefined ||
|
||||
serializer.stringify(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];
|
||||
};
|
@ -6,8 +6,6 @@
|
||||
* 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 qs from "qs";
|
||||
|
||||
export const buildURL = (baseUrl: string, relativePath: string): string => {
|
||||
try {
|
||||
return new URL(relativePath).toString();
|
||||
@ -39,12 +37,3 @@ export const isAbsoluteUrl = (value: string = ""): boolean => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// todo: in the future we might need to extract this logic into a hook
|
||||
export const extractQueryParamsUrl = (fullUrl: string): string => {
|
||||
const extractedQueryParams = qs.parse(new URL(fullUrl).search, {
|
||||
ignoreQueryPrefix: true,
|
||||
});
|
||||
|
||||
return qs.stringify(extractedQueryParams);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user