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 },
|
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:
|
* 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.
|
||||||
* 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 ClearIcon from "@mui/icons-material/Clear";
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
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 { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
|
||||||
import { Adornment } from "./Adornment";
|
import { Adornment } from "./Adornment";
|
||||||
import { Input } from "./Input";
|
import { Input } from "./Input";
|
||||||
|
|
||||||
export const FilterTextfield = (props: TextFieldProps) => {
|
export interface FilterTextFieldProps
|
||||||
const { t } = useTranslate();
|
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 (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
inputRef={ref}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: <Adornment Icon={SearchIcon} />,
|
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")}
|
placeholder={t("placeholder.keywords")}
|
||||||
{...props}
|
{...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:
|
* 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.
|
||||||
* 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 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 (
|
return (
|
||||||
<svg
|
<svg
|
||||||
height="150"
|
|
||||||
viewBox="0 0 184 152"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
width={width}
|
||||||
|
viewBox="0 0 452 257"
|
||||||
|
aria-hidden
|
||||||
|
focusable="false"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<title>No data</title>
|
<path
|
||||||
<g fill="none" fillRule="evenodd">
|
className="no-rows-primary"
|
||||||
<g transform="translate(24 31.67)">
|
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"
|
||||||
<ellipse
|
|
||||||
fillOpacity=".8"
|
|
||||||
fill="#F5F5F7"
|
|
||||||
cx="67.797"
|
|
||||||
cy="106.89"
|
|
||||||
rx="67.797"
|
|
||||||
ry="12.668"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<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"
|
className="no-rows-primary"
|
||||||
fill="#AEB8C2"
|
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
|
<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"
|
className="no-rows-primary"
|
||||||
fill="url(#linearGradient-1)"
|
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"
|
||||||
transform="translate(13.56)"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<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"
|
className="no-rows-secondary"
|
||||||
fill="#F5F5F7"
|
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>
|
</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:
|
* 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.
|
||||||
@ -73,6 +73,12 @@ export const DataGrid = <T extends GridValidRowModel = any>({
|
|||||||
autoHeight={autoHeight}
|
autoHeight={autoHeight}
|
||||||
disableRowSelectionOnClick={disableRowSelectionOnClick}
|
disableRowSelectionOnClick={disableRowSelectionOnClick}
|
||||||
slots={slots}
|
slots={slots}
|
||||||
|
slotProps={{
|
||||||
|
loadingOverlay: {
|
||||||
|
variant: "linear-progress",
|
||||||
|
noRowsVariant: "skeleton",
|
||||||
|
},
|
||||||
|
}}
|
||||||
showCellVerticalBorder={showCellVerticalBorder}
|
showCellVerticalBorder={showCellVerticalBorder}
|
||||||
showColumnVerticalBorder={showColumnVerticalBorder}
|
showColumnVerticalBorder={showColumnVerticalBorder}
|
||||||
sx={sx}
|
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:
|
* 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.
|
||||||
* 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 { Grid, Typography } from "@mui/material";
|
import Box from "@mui/material/Box";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
|
||||||
import NoDataIcon from "../svg/NoDataIcon";
|
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 = () => {
|
export const NoDataOverlay = () => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<StyledGridOverlay>
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "fit-content",
|
|
||||||
gap: 1,
|
|
||||||
opacity: 0.5,
|
|
||||||
paddingY: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NoDataIcon />
|
<NoDataIcon />
|
||||||
<Grid item>
|
<Box sx={{ mt: 2 }}>{t("label.no_data")}</Box>
|
||||||
<Typography
|
</StyledGridOverlay>
|
||||||
style={{
|
|
||||||
color: "text.secondary",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("label.no_data")}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -43,9 +43,12 @@ export const Categories = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { onSearch, searchPayload } = useSearch<ICategory>({
|
const { onSearch, searchPayload, searchText } = useSearch<ICategory>(
|
||||||
|
{
|
||||||
$iLike: ["label"],
|
$iLike: ["label"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.CATEGORY },
|
{ entity: EntityType.CATEGORY },
|
||||||
{
|
{
|
||||||
@ -142,7 +145,7 @@ export const Categories = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasPermission(EntityType.CATEGORY, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.CATEGORY, PermissionAction.CREATE) ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
@ -40,9 +40,12 @@ export const ContentTypes = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
// data fetching
|
// data fetching
|
||||||
const { onSearch, searchPayload } = useSearch<IContentType>({
|
const { onSearch, searchPayload, searchText } = useSearch<IContentType>(
|
||||||
|
{
|
||||||
$iLike: ["name"],
|
$iLike: ["name"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.CONTENT_TYPE },
|
{ entity: EntityType.CONTENT_TYPE },
|
||||||
{
|
{
|
||||||
@ -99,7 +102,7 @@ export const ContentTypes = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasPermission(EntityType.CONTENT_TYPE, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.CONTENT_TYPE, PermissionAction.CREATE) ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
@ -57,10 +57,13 @@ export const Contents = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
// data fetching
|
// data fetching
|
||||||
const { onSearch, searchPayload } = useSearch<IContent>({
|
const { onSearch, searchPayload, searchText } = useSearch<IContent>(
|
||||||
|
{
|
||||||
$eq: [{ entity: String(query.id) }],
|
$eq: [{ entity: String(query.id) }],
|
||||||
$iLike: ["title"],
|
$iLike: ["title"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.CONTENT, format: Format.FULL },
|
{ entity: EntityType.CONTENT, format: Format.FULL },
|
||||||
@ -157,7 +160,7 @@ export const Contents = () => {
|
|||||||
>
|
>
|
||||||
<Grid justifyContent="flex-end" gap={1} container alignItems="center">
|
<Grid justifyContent="flex-end" gap={1} container alignItems="center">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? (
|
||||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||||
|
@ -43,9 +43,12 @@ export const ContextVars = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { onSearch, searchPayload } = useSearch<IContextVar>({
|
const { onSearch, searchPayload, searchText } = useSearch<IContextVar>(
|
||||||
|
{
|
||||||
$iLike: ["label"],
|
$iLike: ["label"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.CONTEXT_VAR },
|
{ entity: EntityType.CONTEXT_VAR },
|
||||||
{
|
{
|
||||||
@ -176,7 +179,7 @@ export const ContextVars = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasPermission(EntityType.CONTEXT_VAR, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.CONTEXT_VAR, PermissionAction.CREATE) ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
@ -21,7 +21,6 @@ import { useTranslate } from "@/hooks/useTranslate";
|
|||||||
import { Title } from "@/layout/content/Title";
|
import { Title } from "@/layout/content/Title";
|
||||||
import { EntityType, RouterType } from "@/services/types";
|
import { EntityType, RouterType } from "@/services/types";
|
||||||
import { normalizeDate } from "@/utils/date";
|
import { normalizeDate } from "@/utils/date";
|
||||||
import { extractQueryParamsUrl } from "@/utils/URL";
|
|
||||||
|
|
||||||
import { getAvatarSrc } from "../helpers/mapMessages";
|
import { getAvatarSrc } from "../helpers/mapMessages";
|
||||||
import { useChat } from "../hooks/ChatContext";
|
import { useChat } from "../hooks/ChatContext";
|
||||||
@ -33,8 +32,8 @@ export const SubscribersList = (props: {
|
|||||||
searchPayload: any;
|
searchPayload: any;
|
||||||
assignedTo: AssignedTo;
|
assignedTo: AssignedTo;
|
||||||
}) => {
|
}) => {
|
||||||
const { query, push } = useRouter();
|
const router = useRouter();
|
||||||
const subscriber = query.subscriber?.toString() || null;
|
const subscriber = router.query.subscriber?.toString() || null;
|
||||||
const { apiUrl } = useConfig();
|
const { apiUrl } = useConfig();
|
||||||
const { t, i18n } = useTranslate();
|
const { t, i18n } = useTranslate();
|
||||||
const chat = useChat();
|
const chat = useChat();
|
||||||
@ -75,10 +74,17 @@ export const SubscribersList = (props: {
|
|||||||
<Conversation
|
<Conversation
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
chat.setSubscriberId(subscriber.id);
|
chat.setSubscriberId(subscriber.id);
|
||||||
push({
|
router.push(
|
||||||
pathname: `/${RouterType.INBOX}/subscribers/${subscriber.id}`,
|
{
|
||||||
query: extractQueryParamsUrl(window.location.href),
|
pathname: `/${RouterType.INBOX}/subscribers/[subscriber]`,
|
||||||
});
|
query: {
|
||||||
|
...router.query,
|
||||||
|
subscriber: subscriber.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true },
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
className="changeColor"
|
className="changeColor"
|
||||||
key={subscriber.id}
|
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).
|
* 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,9 +27,12 @@ import { AssignedTo } from "./types";
|
|||||||
|
|
||||||
export const Inbox = () => {
|
export const Inbox = () => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>({
|
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>(
|
||||||
|
{
|
||||||
$or: ["first_name", "last_name"],
|
$or: ["first_name", "last_name"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
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,13 +50,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} paddingTop={2} paddingBottom={1} mx={1}>
|
||||||
<Search
|
<FilterTextfield
|
||||||
value={searchText}
|
onChange={onSearch}
|
||||||
onClearClick={() => onSearch("")}
|
defaultValue={searchText}
|
||||||
className="changeColor"
|
|
||||||
onChange={(v) => onSearch(v)}
|
|
||||||
placeholder="Search..."
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
|
@ -42,9 +42,12 @@ export const Labels = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { onSearch, searchPayload } = useSearch<ILabel>({
|
const { onSearch, searchPayload, searchText } = useSearch<ILabel>(
|
||||||
|
{
|
||||||
$or: ["name", "title"],
|
$or: ["name", "title"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.LABEL, format: Format.FULL },
|
{ entity: EntityType.LABEL, format: Format.FULL },
|
||||||
{
|
{
|
||||||
@ -173,7 +176,7 @@ export const Labels = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasPermission(EntityType.LABEL, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.LABEL, PermissionAction.CREATE) ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
@ -43,9 +43,12 @@ export const Languages = () => {
|
|||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { onSearch, searchPayload } = useSearch<ILanguage>({
|
const { onSearch, searchPayload, searchText } = useSearch<ILanguage>(
|
||||||
|
{
|
||||||
$or: ["title", "code"],
|
$or: ["title", "code"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps, refetch } = useFind(
|
const { dataGridProps, refetch } = useFind(
|
||||||
{ entity: EntityType.LANGUAGE },
|
{ entity: EntityType.LANGUAGE },
|
||||||
{
|
{
|
||||||
@ -197,7 +200,7 @@ export const Languages = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
@ -37,9 +37,13 @@ type MediaLibraryProps = {
|
|||||||
export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const formatFileSize = useFormattedFileSize();
|
const formatFileSize = useFormattedFileSize();
|
||||||
const { onSearch, searchPayload } = useSearch<IAttachment>({
|
const { onSearch, searchPayload, searchText } = useSearch<IAttachment>(
|
||||||
|
{
|
||||||
$iLike: ["name"],
|
$iLike: ["name"],
|
||||||
});
|
},
|
||||||
|
// Sync URL only in the media library page (not the modal)
|
||||||
|
{ syncUrl: !onSelect },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.ATTACHMENT },
|
{ entity: EntityType.ATTACHMENT },
|
||||||
{
|
{
|
||||||
@ -151,7 +155,7 @@ export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
@ -80,9 +80,12 @@ const NlpEntity = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [selectedNlpEntities, setSelectedNlpEntities] = useState<string[]>([]);
|
const [selectedNlpEntities, setSelectedNlpEntities] = useState<string[]>([]);
|
||||||
const { onSearch, searchPayload } = useSearch<INlpEntity>({
|
const { onSearch, searchPayload, searchText } = useSearch<INlpEntity>(
|
||||||
|
{
|
||||||
$or: ["name", "doc"],
|
$or: ["name", "doc"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps: nlpEntityGrid } = useFind(
|
const { dataGridProps: nlpEntityGrid } = useFind(
|
||||||
{
|
{
|
||||||
entity: EntityType.NLP_ENTITY,
|
entity: EntityType.NLP_ENTITY,
|
||||||
@ -224,7 +227,7 @@ const NlpEntity = () => {
|
|||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{hasPermission(EntityType.NLP_ENTITY, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.NLP_ENTITY, PermissionAction.CREATE) ? (
|
||||||
|
@ -86,13 +86,16 @@ export default function NlpSample() {
|
|||||||
EntityType.NLP_SAMPLE_ENTITY,
|
EntityType.NLP_SAMPLE_ENTITY,
|
||||||
);
|
);
|
||||||
const getLanguageFromCache = useGetFromCache(EntityType.LANGUAGE);
|
const getLanguageFromCache = useGetFromCache(EntityType.LANGUAGE);
|
||||||
const { onSearch, searchPayload } = useSearch<INlpSample>({
|
const { onSearch, searchPayload, searchText } = useSearch<INlpSample>(
|
||||||
|
{
|
||||||
$eq: [
|
$eq: [
|
||||||
...(type !== "all" ? [{ type }] : []),
|
...(type !== "all" ? [{ type }] : []),
|
||||||
...(language ? [{ language }] : []),
|
...(language ? [{ language }] : []),
|
||||||
],
|
],
|
||||||
$iLike: ["text"],
|
$iLike: ["text"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { mutate: deleteNlpSample } = useDelete(EntityType.NLP_SAMPLE, {
|
const { mutate: deleteNlpSample } = useDelete(EntityType.NLP_SAMPLE, {
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error(t("message.internal_server_error"));
|
toast.error(t("message.internal_server_error"));
|
||||||
@ -317,6 +320,7 @@ export default function NlpSample() {
|
|||||||
>
|
>
|
||||||
<FilterTextfield
|
<FilterTextfield
|
||||||
onChange={onSearch}
|
onChange={onSearch}
|
||||||
|
defaultValue={searchText}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
sx={{ minWidth: "256px" }}
|
sx={{ minWidth: "256px" }}
|
||||||
/>
|
/>
|
||||||
|
@ -51,10 +51,13 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
|||||||
entity: EntityType.NLP_ENTITY,
|
entity: EntityType.NLP_ENTITY,
|
||||||
format: Format.FULL,
|
format: Format.FULL,
|
||||||
});
|
});
|
||||||
const { onSearch, searchPayload } = useSearch<INlpValue>({
|
const { onSearch, searchPayload, searchText } = useSearch<INlpValue>(
|
||||||
|
{
|
||||||
$eq: [{ entity: entityId }],
|
$eq: [{ entity: entityId }],
|
||||||
$or: ["doc", "value"],
|
$or: ["doc", "value"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.NLP_VALUE, format: Format.FULL },
|
{ entity: EntityType.NLP_VALUE, format: Format.FULL },
|
||||||
{
|
{
|
||||||
@ -228,7 +231,10 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
|||||||
sx={{ width: "max-content", gap: 1 }}
|
sx={{ width: "max-content", gap: 1 }}
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield
|
||||||
|
onChange={onSearch}
|
||||||
|
defaultValue={searchText}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||||
{hasPermission(
|
{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:
|
* 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.
|
||||||
@ -40,9 +40,12 @@ export const Roles = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { onSearch, searchPayload } = useSearch<IRole>({
|
const { onSearch, searchPayload, searchText } = useSearch<IRole>(
|
||||||
|
{
|
||||||
$iLike: ["name"],
|
$iLike: ["name"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.ROLE },
|
{ entity: EntityType.ROLE },
|
||||||
{
|
{
|
||||||
@ -140,7 +143,7 @@ export const Roles = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{hasPermission(EntityType.ROLE, PermissionAction.CREATE) ? (
|
{hasPermission(EntityType.ROLE, PermissionAction.CREATE) ? (
|
||||||
<Grid item>
|
<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:
|
* 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.
|
||||||
@ -43,10 +43,13 @@ export const Subscribers = () => {
|
|||||||
{ hasCount: false },
|
{ hasCount: false },
|
||||||
);
|
);
|
||||||
const [labelFilter, setLabelFilter] = useState<string>("");
|
const [labelFilter, setLabelFilter] = useState<string>("");
|
||||||
const { onSearch, searchPayload } = useSearch<ISubscriber>({
|
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>(
|
||||||
|
{
|
||||||
$eq: labelFilter ? [{ labels: [labelFilter] }] : [],
|
$eq: labelFilter ? [{ labels: [labelFilter] }] : [],
|
||||||
$or: ["first_name", "last_name"],
|
$or: ["first_name", "last_name"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps } = useFind(
|
const { dataGridProps } = useFind(
|
||||||
{ entity: EntityType.SUBSCRIBER, format: Format.FULL },
|
{ entity: EntityType.SUBSCRIBER, format: Format.FULL },
|
||||||
{ params: searchPayload },
|
{ params: searchPayload },
|
||||||
@ -172,7 +175,11 @@ export const Subscribers = () => {
|
|||||||
flexWrap="nowrap"
|
flexWrap="nowrap"
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
<FilterTextfield onChange={onSearch} fullWidth={true} />
|
<FilterTextfield
|
||||||
|
onChange={onSearch}
|
||||||
|
fullWidth={true}
|
||||||
|
defaultValue={searchText}
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
select
|
select
|
||||||
label={t("label.labels")}
|
label={t("label.labels")}
|
||||||
|
@ -44,9 +44,12 @@ export const Translations = () => {
|
|||||||
hasCount: false,
|
hasCount: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { onSearch, searchPayload } = useSearch<ITranslation>({
|
const { onSearch, searchPayload, searchText } = useSearch<ITranslation>(
|
||||||
|
{
|
||||||
$iLike: ["str"],
|
$iLike: ["str"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { dataGridProps, refetch: refreshTranslations } = useFind(
|
const { dataGridProps, refetch: refreshTranslations } = useFind(
|
||||||
{ entity: EntityType.TRANSLATION },
|
{ entity: EntityType.TRANSLATION },
|
||||||
{
|
{
|
||||||
@ -152,7 +155,7 @@ export const Translations = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button
|
<Button
|
||||||
|
@ -53,9 +53,12 @@ export const Users = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { onSearch, searchPayload } = useSearch<IUser>({
|
const { onSearch, searchPayload, searchText } = useSearch<IUser>(
|
||||||
|
{
|
||||||
$or: ["first_name", "last_name", "email"],
|
$or: ["first_name", "last_name", "email"],
|
||||||
});
|
},
|
||||||
|
{ syncUrl: true },
|
||||||
|
);
|
||||||
const { data: roles } = useFind(
|
const { data: roles } = useFind(
|
||||||
{
|
{
|
||||||
entity: EntityType.ROLE,
|
entity: EntityType.ROLE,
|
||||||
@ -102,7 +105,7 @@ export const Users = () => {
|
|||||||
headerName: t("label.name"),
|
headerName: t("label.name"),
|
||||||
sortable: false,
|
sortable: false,
|
||||||
disableColumnMenu: true,
|
disableColumnMenu: true,
|
||||||
valueGetter: (params, val) => `${val.first_name} ${val.last_name}`,
|
valueGetter: (_params, val) => `${val.first_name} ${val.last_name}`,
|
||||||
headerAlign: "left",
|
headerAlign: "left",
|
||||||
renderHeader,
|
renderHeader,
|
||||||
},
|
},
|
||||||
@ -198,7 +201,7 @@ export const Users = () => {
|
|||||||
width="max-content"
|
width="max-content"
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FilterTextfield onChange={onSearch} />
|
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||||
</Grid>
|
</Grid>
|
||||||
{!ssoEnabled &&
|
{!ssoEnabled &&
|
||||||
hasPermission(EntityType.USER, PermissionAction.CREATE) ? (
|
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:
|
* 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.
|
||||||
@ -86,6 +86,7 @@ export const useFind = <
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
keepPreviousData: true,
|
||||||
...otherOptions,
|
...otherOptions,
|
||||||
});
|
});
|
||||||
const data = (ids || [])
|
const data = (ids || [])
|
||||||
@ -99,7 +100,7 @@ export const useFind = <
|
|||||||
dataGridProps: {
|
dataGridProps: {
|
||||||
...dataGridPaginationProps,
|
...dataGridPaginationProps,
|
||||||
rows: data || [],
|
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:
|
* 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.
|
||||||
@ -74,6 +74,7 @@ export const useInfiniteFind = <
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
keepPreviousData: true,
|
||||||
...(otherOptions || {}),
|
...(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).
|
* 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 { ChangeEvent, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { ChangeEvent, useCallback, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TBuildInitialParamProps,
|
TBuildInitialParamProps,
|
||||||
@ -16,6 +14,8 @@ import {
|
|||||||
TParamItem,
|
TParamItem,
|
||||||
} from "@/types/search.types";
|
} from "@/types/search.types";
|
||||||
|
|
||||||
|
import { useUrlQueryParam } from "./useUrlQueryParam";
|
||||||
|
|
||||||
const buildOrParams = <T,>({ params, searchText }: TBuildParamProps<T>) => ({
|
const buildOrParams = <T,>({ params, searchText }: TBuildParamProps<T>) => ({
|
||||||
or: params?.map((field) => ({
|
or: params?.map((field) => ({
|
||||||
[field]: { contains: searchText },
|
[field]: { contains: searchText },
|
||||||
@ -52,49 +52,39 @@ const buildNeqInitialParams = <T,>({
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useSearch = <T,>(params: TParamItem<T>) => {
|
interface SearchHookOptions {
|
||||||
const router = useRouter();
|
syncUrl?: boolean;
|
||||||
const [searchText, setSearchText] = useState<string>(
|
}
|
||||||
(router.query.search as string) || "",
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const useSearch = <T,>(
|
||||||
if (router.query.search !== searchText) {
|
params: TParamItem<T>,
|
||||||
setSearchText((router.query.search as string) || "");
|
options: SearchHookOptions = { syncUrl: false },
|
||||||
}
|
) => {
|
||||||
}, [router.query.search]);
|
const { syncUrl } = options;
|
||||||
|
const [searchQuery, setSearchQuery] = useUrlQueryParam("search", "");
|
||||||
const updateQueryParams = useCallback(
|
const [search, setSearch] = useState<string>("");
|
||||||
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 {
|
const {
|
||||||
$eq: eqInitialParams,
|
$eq: eqInitialParams,
|
||||||
$iLike: iLikeParams,
|
$iLike: iLikeParams,
|
||||||
$neq: neqInitialParams,
|
$neq: neqInitialParams,
|
||||||
$or: orParams,
|
$or: orParams,
|
||||||
} = params;
|
} = params;
|
||||||
|
const searchText = syncUrl ? searchQuery : search;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchText,
|
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: {
|
searchPayload: {
|
||||||
where: {
|
where: {
|
||||||
...buildEqInitialParams({ initialParams: eqInitialParams }),
|
...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).
|
* 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 => {
|
export const buildURL = (baseUrl: string, relativePath: string): string => {
|
||||||
try {
|
try {
|
||||||
return new URL(relativePath).toString();
|
return new URL(relativePath).toString();
|
||||||
@ -39,12 +37,3 @@ export const isAbsoluteUrl = (value: string = ""): boolean => {
|
|||||||
return false;
|
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