diff --git a/frontend/src/app-components/attachment/AttachmentUploader.tsx b/frontend/src/app-components/attachment/AttachmentUploader.tsx index ef5eb377..b49dd316 100644 --- a/frontend/src/app-components/attachment/AttachmentUploader.tsx +++ b/frontend/src/app-components/attachment/AttachmentUploader.tsx @@ -222,7 +222,7 @@ const AttachmentUploader: FC = ({ { defaultValues: { accept, onChange }, }, - { maxWidth: "xl" }, + { maxWidth: "xl", isSingleton: true }, ) } > diff --git a/frontend/src/app-components/inputs/FilterTextfield.tsx b/frontend/src/app-components/inputs/FilterTextfield.tsx index bbd3af20..0d7cc302 100644 --- a/frontend/src/app-components/inputs/FilterTextfield.tsx +++ b/frontend/src/app-components/inputs/FilterTextfield.tsx @@ -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(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) => { 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 ( , endAdornment: clearable && ( @@ -73,7 +92,8 @@ export const FilterTextfield = ({ }} placeholder={t("placeholder.keywords")} {...props} - value={inputValue} + // value={inputValue} + defaultValue={defaultValue} onChange={handleChange} /> ); diff --git a/frontend/src/components/categories/index.tsx b/frontend/src/components/categories/index.tsx index 18e46f09..c9c1c748 100644 --- a/frontend/src/components/categories/index.tsx +++ b/frontend/src/components/categories/index.tsx @@ -43,9 +43,12 @@ export const Categories = () => { const { toast } = useToast(); const dialogs = useDialogs(); const hasPermission = useHasPermission(); - const { onSearch, searchPayload } = useSearch({ - $iLike: ["label"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $iLike: ["label"], + }, + { syncUrl: true }, + ); const { dataGridProps } = useFind( { entity: EntityType.CATEGORY }, { @@ -142,7 +145,7 @@ export const Categories = () => { width="max-content" > - + {hasPermission(EntityType.CATEGORY, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/content-types/index.tsx b/frontend/src/components/content-types/index.tsx index 69fe5827..951cb00f 100644 --- a/frontend/src/components/content-types/index.tsx +++ b/frontend/src/components/content-types/index.tsx @@ -40,9 +40,12 @@ export const ContentTypes = () => { const router = useRouter(); const dialogs = useDialogs(); // data fetching - const { onSearch, searchPayload } = useSearch({ - $iLike: ["name"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $iLike: ["name"], + }, + { syncUrl: true }, + ); const { dataGridProps } = useFind( { entity: EntityType.CONTENT_TYPE }, { @@ -99,7 +102,7 @@ export const ContentTypes = () => { width="max-content" > - + {hasPermission(EntityType.CONTENT_TYPE, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/contents/index.tsx b/frontend/src/components/contents/index.tsx index e95d5111..e577d9de 100644 --- a/frontend/src/components/contents/index.tsx +++ b/frontend/src/components/contents/index.tsx @@ -57,10 +57,13 @@ export const Contents = () => { const queryClient = useQueryClient(); const dialogs = useDialogs(); // data fetching - const { onSearch, searchPayload } = useSearch({ - $eq: [{ entity: String(query.id) }], - $iLike: ["title"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $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 = () => { > - + {hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/context-vars/index.tsx b/frontend/src/components/context-vars/index.tsx index c2a617b6..1095b6a8 100644 --- a/frontend/src/components/context-vars/index.tsx +++ b/frontend/src/components/context-vars/index.tsx @@ -43,9 +43,12 @@ export const ContextVars = () => { const { toast } = useToast(); const dialogs = useDialogs(); const hasPermission = useHasPermission(); - const { onSearch, searchPayload } = useSearch({ - $iLike: ["label"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $iLike: ["label"], + }, + { syncUrl: true }, + ); const { dataGridProps } = useFind( { entity: EntityType.CONTEXT_VAR }, { @@ -176,7 +179,7 @@ export const ContextVars = () => { width="max-content" > - + {hasPermission(EntityType.CONTEXT_VAR, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/inbox/index.tsx b/frontend/src/components/inbox/index.tsx index 598b7461..22564d47 100644 --- a/frontend/src/components/inbox/index.tsx +++ b/frontend/src/components/inbox/index.tsx @@ -27,9 +27,12 @@ import { AssignedTo } from "./types"; export const Inbox = () => { const { t } = useTranslate(); - const { onSearch, searchPayload } = useSearch({ - $or: ["first_name", "last_name"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $or: ["first_name", "last_name"], + }, + { syncUrl: true }, + ); const [channels, setChannels] = useState([]); const [assignment, setAssignment] = useState(AssignedTo.ALL); @@ -48,13 +51,10 @@ export const Inbox = () => { - - {/* onSearch("")} - className="changeColor" - onChange={(v) => onSearch(v)} - placeholder="Search..." - /> */} + { const { toast } = useToast(); const dialogs = useDialogs(); const hasPermission = useHasPermission(); - const { onSearch, searchPayload } = useSearch({ - $or: ["name", "title"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $or: ["name", "title"], + }, + { syncUrl: true }, + ); const { dataGridProps } = useFind( { entity: EntityType.LABEL, format: Format.FULL }, { @@ -173,7 +176,7 @@ export const Labels = () => { width="max-content" > - + {hasPermission(EntityType.LABEL, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/languages/index.tsx b/frontend/src/components/languages/index.tsx index 74b6be11..c14da822 100644 --- a/frontend/src/components/languages/index.tsx +++ b/frontend/src/components/languages/index.tsx @@ -43,9 +43,12 @@ export const Languages = () => { const dialogs = useDialogs(); const queryClient = useQueryClient(); const hasPermission = useHasPermission(); - const { onSearch, searchPayload } = useSearch({ - $or: ["title", "code"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $or: ["title", "code"], + }, + { syncUrl: true }, + ); const { dataGridProps, refetch } = useFind( { entity: EntityType.LANGUAGE }, { @@ -197,7 +200,7 @@ export const Languages = () => { width="max-content" > - + {hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/media-library/index.tsx b/frontend/src/components/media-library/index.tsx index 15ea6854..847c7fbe 100644 --- a/frontend/src/components/media-library/index.tsx +++ b/frontend/src/components/media-library/index.tsx @@ -37,9 +37,13 @@ type MediaLibraryProps = { export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => { const { t } = useTranslate(); const formatFileSize = useFormattedFileSize(); - const { onSearch, searchPayload } = useSearch({ - $iLike: ["name"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $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" > - + diff --git a/frontend/src/components/nlp/components/NlpEntity.tsx b/frontend/src/components/nlp/components/NlpEntity.tsx index f039170f..a8e76cd1 100644 --- a/frontend/src/components/nlp/components/NlpEntity.tsx +++ b/frontend/src/components/nlp/components/NlpEntity.tsx @@ -80,9 +80,12 @@ const NlpEntity = () => { }, }); const [selectedNlpEntities, setSelectedNlpEntities] = useState([]); - const { onSearch, searchPayload } = useSearch({ - $or: ["name", "doc"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $or: ["name", "doc"], + }, + { syncUrl: true }, + ); const { dataGridProps: nlpEntityGrid } = useFind( { entity: EntityType.NLP_ENTITY, @@ -224,7 +227,7 @@ const NlpEntity = () => { flexShrink={0} > - + {hasPermission(EntityType.NLP_ENTITY, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/nlp/components/NlpSample.tsx b/frontend/src/components/nlp/components/NlpSample.tsx index c9f214f9..49b3eca1 100644 --- a/frontend/src/components/nlp/components/NlpSample.tsx +++ b/frontend/src/components/nlp/components/NlpSample.tsx @@ -86,13 +86,16 @@ export default function NlpSample() { EntityType.NLP_SAMPLE_ENTITY, ); const getLanguageFromCache = useGetFromCache(EntityType.LANGUAGE); - const { onSearch, searchPayload } = useSearch({ - $eq: [ - ...(type !== "all" ? [{ type }] : []), - ...(language ? [{ language }] : []), - ], - $iLike: ["text"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $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() { > diff --git a/frontend/src/components/nlp/components/NlpValue.tsx b/frontend/src/components/nlp/components/NlpValue.tsx index aac1c9a3..789a96a4 100644 --- a/frontend/src/components/nlp/components/NlpValue.tsx +++ b/frontend/src/components/nlp/components/NlpValue.tsx @@ -51,10 +51,13 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { entity: EntityType.NLP_ENTITY, format: Format.FULL, }); - const { onSearch, searchPayload } = useSearch({ - $eq: [{ entity: entityId }], - $or: ["doc", "value"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $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 }} > - + {hasPermission( diff --git a/frontend/src/components/roles/index.tsx b/frontend/src/components/roles/index.tsx index 2d2928c1..d4c4f568 100644 --- a/frontend/src/components/roles/index.tsx +++ b/frontend/src/components/roles/index.tsx @@ -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({ - $iLike: ["name"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $iLike: ["name"], + }, + { syncUrl: true }, + ); const { dataGridProps } = useFind( { entity: EntityType.ROLE }, { @@ -140,7 +143,7 @@ export const Roles = () => { width="max-content" > - + {hasPermission(EntityType.ROLE, PermissionAction.CREATE) ? ( diff --git a/frontend/src/components/subscribers/index.tsx b/frontend/src/components/subscribers/index.tsx index 23cf33d2..3c03078c 100644 --- a/frontend/src/components/subscribers/index.tsx +++ b/frontend/src/components/subscribers/index.tsx @@ -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(""); - const { onSearch, searchPayload } = useSearch({ - $eq: labelFilter ? [{ labels: [labelFilter] }] : [], - $or: ["first_name", "last_name"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $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%" > - + { hasCount: false, }, ); - const { onSearch, searchPayload } = useSearch({ - $iLike: ["str"], - }); + const { onSearch, searchPayload, searchText } = useSearch( + { + $iLike: ["str"], + }, + { syncUrl: true }, + ); const { dataGridProps, refetch: refreshTranslations } = useFind( { entity: EntityType.TRANSLATION }, { @@ -152,7 +155,7 @@ export const Translations = () => { width="max-content" > - +