From 8d38b796361e8dc3646b0fd2fdcd10057665023c Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 23 May 2025 11:40:47 +0100 Subject: [PATCH 01/53] fix(frontend): patch AutoCompleteSelect component key --- frontend/src/app-components/inputs/AutoCompleteSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app-components/inputs/AutoCompleteSelect.tsx b/frontend/src/app-components/inputs/AutoCompleteSelect.tsx index e0d5f1a9..a5fe9667 100644 --- a/frontend/src/app-components/inputs/AutoCompleteSelect.tsx +++ b/frontend/src/app-components/inputs/AutoCompleteSelect.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. @@ -112,7 +112,7 @@ const AutoCompleteSelect = < {...rest} ref={ref} size="small" - key={JSON.stringify(value)} + key={`${JSON.stringify(options)}_${JSON.stringify(value)}`} disabled={isDisabled} defaultValue={selected} multiple={multiple} From 69ed2eb552547e3cbf203d3836d4d29131bdfaf9 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 23 May 2025 11:41:37 +0100 Subject: [PATCH 02/53] fix(frontend): update AutoCompleteSelectEntity value --- frontend/src/app-components/inputs/AutoCompleteEntitySelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app-components/inputs/AutoCompleteEntitySelect.tsx b/frontend/src/app-components/inputs/AutoCompleteEntitySelect.tsx index 9c8d8725..840a3d31 100644 --- a/frontend/src/app-components/inputs/AutoCompleteEntitySelect.tsx +++ b/frontend/src/app-components/inputs/AutoCompleteEntitySelect.tsx @@ -106,7 +106,7 @@ const AutoCompleteEntitySelect = < return ( - {...(options.length && { value })} + value={value} onChange={onChange} label={label} multiple={multiple} From 1c802cd67f00d2b99b8122ece76000babb31f77a Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 23 May 2025 11:42:08 +0100 Subject: [PATCH 03/53] fix(frontend): update previous related updates --- .../visual-editor/form/ListMessageForm.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/visual-editor/form/ListMessageForm.tsx b/frontend/src/components/visual-editor/form/ListMessageForm.tsx index ac5989b8..10ca9703 100644 --- a/frontend/src/components/visual-editor/form/ListMessageForm.tsx +++ b/frontend/src/components/visual-editor/form/ListMessageForm.tsx @@ -182,7 +182,7 @@ const ListMessageForm = () => { }} defaultValue={content?.fields?.title} render={({ field }) => { - const { onChange, value, ...rest } = field; + const { onChange, ...rest } = field; const options = (contentType?.fields || []).filter( ({ type }) => ContentFieldType.TEXT === type, ); @@ -194,7 +194,6 @@ const ListMessageForm = () => { labelKey="label" label={t("label.title")} multiple={false} - {...(options.length && { value })} {...rest} onChange={(_e, selected) => onChange(selected?.name)} error={!!errors?.options?.["content"]?.fields?.title} @@ -212,7 +211,7 @@ const ListMessageForm = () => { control={control} defaultValue={content?.fields?.subtitle} render={({ field }) => { - const { onChange, value, ...rest } = field; + const { onChange, ...rest } = field; const options = (contentType?.fields || []).filter( ({ type }) => ContentFieldType.TEXT === type || @@ -227,7 +226,6 @@ const ListMessageForm = () => { label={t("label.subtitle")} multiple={false} onChange={(_e, selected) => onChange(selected?.name)} - {...(options.length && { value })} {...rest} /> ); @@ -240,7 +238,7 @@ const ListMessageForm = () => { control={control} defaultValue={content?.fields?.image_url} render={({ field }) => { - const { onChange, value, ...rest } = field; + const { onChange, ...rest } = field; const options = (contentType?.fields || []).filter(({ type }) => [ContentFieldType.FILE].includes(type), ); @@ -253,7 +251,6 @@ const ListMessageForm = () => { label={t("label.image_url")} multiple={false} onChange={(_e, selected) => onChange(selected?.name)} - {...(options.length && { value })} {...rest} /> ); @@ -266,7 +263,7 @@ const ListMessageForm = () => { control={control} defaultValue={content?.fields?.url} render={({ field }) => { - const { onChange, value, ...rest } = field; + const { onChange, ...rest } = field; const options = (contentType?.fields || []).filter(({ type }) => [ContentFieldType.URL].includes(type), ); @@ -279,7 +276,6 @@ const ListMessageForm = () => { label={t("label.url")} multiple={false} onChange={(_e, selected) => onChange(selected?.name)} - {...(options.length && { value })} {...rest} /> ); From 209fe8e0ee7d324526c49c0015b1e781a58227ec Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 23 May 2025 11:59:23 +0100 Subject: [PATCH 04/53] fix(frontend): use fast stringify function --- frontend/src/app-components/inputs/AutoCompleteSelect.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app-components/inputs/AutoCompleteSelect.tsx b/frontend/src/app-components/inputs/AutoCompleteSelect.tsx index a5fe9667..439cc6d5 100644 --- a/frontend/src/app-components/inputs/AutoCompleteSelect.tsx +++ b/frontend/src/app-components/inputs/AutoCompleteSelect.tsx @@ -11,6 +11,7 @@ import Autocomplete, { AutocompleteProps, AutocompleteValue, } from "@mui/material/Autocomplete"; +import stringify from "fast-json-stable-stringify"; import { forwardRef, useCallback, useMemo } from "react"; import { Input } from "@/app-components/inputs/Input"; @@ -112,7 +113,7 @@ const AutoCompleteSelect = < {...rest} ref={ref} size="small" - key={`${JSON.stringify(options)}_${JSON.stringify(value)}`} + key={`${stringify(options)}_${stringify(value)}`} disabled={isDisabled} defaultValue={selected} multiple={multiple} From c1db0c7ad035742185e3fe6d8597e3b2aa3b9f39 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 23 May 2025 15:41:10 +0100 Subject: [PATCH 05/53] fix(frontend): update incorrect translation keys --- frontend/src/app-components/auth/Register.tsx | 2 +- frontend/src/components/nlp/components/NlpValueForm.tsx | 2 +- frontend/src/components/settings/SettingInput.tsx | 4 ++-- frontend/src/components/users/EditUserForm.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app-components/auth/Register.tsx b/frontend/src/app-components/auth/Register.tsx index e4e9e91d..7f121251 100644 --- a/frontend/src/app-components/auth/Register.tsx +++ b/frontend/src/app-components/auth/Register.tsx @@ -206,7 +206,7 @@ export const Register = () => { > = ({ helperText={errors.metadata?.pattern?.message} error={!!errors.metadata?.pattern} label={t("label.regex")} - placeholder={t("placeholder.pattern")} + placeholder={t("label.pattern")} flags={["i"]} /> diff --git a/frontend/src/components/settings/SettingInput.tsx b/frontend/src/components/settings/SettingInput.tsx index 62f336d7..64836ac9 100644 --- a/frontend/src/components/settings/SettingInput.tsx +++ b/frontend/src/components/settings/SettingInput.tsx @@ -118,8 +118,8 @@ const SettingInput: React.FC = ({ entity={EntityType.BLOCK} format={Format.BASIC} labelKey="name" - label={t("label.fallback_block")} - helperText={t("help.fallback_block")} + label={t("help.fallback_message")} + helperText={t("help.fallback_message")} multiple={false} onChange={(_e, selected, ..._) => onChange(selected?.id || "")} {...rest} diff --git a/frontend/src/components/users/EditUserForm.tsx b/frontend/src/components/users/EditUserForm.tsx index dbf9ad3b..59b26556 100644 --- a/frontend/src/components/users/EditUserForm.tsx +++ b/frontend/src/components/users/EditUserForm.tsx @@ -76,7 +76,7 @@ export const EditUserForm: FC> = ({ Date: Tue, 13 May 2025 15:13:31 +0100 Subject: [PATCH 06/53] fix: block deletion for built Nlp Entities --- api/src/nlp/controllers/nlp-entity.controller.ts | 15 +++++++++++++++ .../src/components/nlp/components/NlpEntity.tsx | 1 + 2 files changed, 16 insertions(+) diff --git a/api/src/nlp/controllers/nlp-entity.controller.ts b/api/src/nlp/controllers/nlp-entity.controller.ts index 1deb0c83..6e439ba5 100644 --- a/api/src/nlp/controllers/nlp-entity.controller.ts +++ b/api/src/nlp/controllers/nlp-entity.controller.ts @@ -222,6 +222,21 @@ export class NlpEntityController extends BaseController< if (!ids?.length) { throw new BadRequestException('No IDs provided for deletion.'); } + + const { count: builtinCount } = await this.filterCount({ + _id: { $in: ids }, + builtin: true, + }); + + if (builtinCount) { + this.logger.warn( + `Unable to delete NLP entities with provided IDs: ${ids}`, + ); + throw new NotFoundException( + 'Cannot delete builtin NLP entities because at least one is built-in', + ); + } + const deleteResult = await this.nlpEntityService.deleteMany({ _id: { $in: ids }, }); diff --git a/frontend/src/components/nlp/components/NlpEntity.tsx b/frontend/src/components/nlp/components/NlpEntity.tsx index 83b7c959..f039170f 100644 --- a/frontend/src/components/nlp/components/NlpEntity.tsx +++ b/frontend/src/components/nlp/components/NlpEntity.tsx @@ -267,6 +267,7 @@ const NlpEntity = () => { !row.builtin} checkboxSelection onRowSelectionModelChange={handleSelectionChange} /> From 7a23b0ded7656f33268c67fa984a9bd11e0c9f9d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 13 May 2025 15:29:39 +0100 Subject: [PATCH 07/53] fix: update throw message --- api/src/nlp/controllers/nlp-entity.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/nlp/controllers/nlp-entity.controller.ts b/api/src/nlp/controllers/nlp-entity.controller.ts index 6e439ba5..9f1f2c94 100644 --- a/api/src/nlp/controllers/nlp-entity.controller.ts +++ b/api/src/nlp/controllers/nlp-entity.controller.ts @@ -223,17 +223,17 @@ export class NlpEntityController extends BaseController< throw new BadRequestException('No IDs provided for deletion.'); } - const { count: builtinCount } = await this.filterCount({ + const { count: builtinNlpEntitiesCount } = await this.filterCount({ _id: { $in: ids }, builtin: true, }); - if (builtinCount) { + if (builtinNlpEntitiesCount > 0) { this.logger.warn( `Unable to delete NLP entities with provided IDs: ${ids}`, ); - throw new NotFoundException( - 'Cannot delete builtin NLP entities because at least one is built-in', + throw new MethodNotAllowedException( + 'Deletion failed: Selection includes built-in NLP entities that are protected', ); } From 2f5c85cb643c847855d8b7d8e7453d93e2e9fb76 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 15 May 2025 13:57:17 +0100 Subject: [PATCH 08/53] fix(api): move builtin logic + adapt unit tests --- api/src/nlp/controllers/nlp-entity.controller.ts | 14 -------------- .../nlp/controllers/nlp-value.controller.spec.ts | 3 +-- api/src/nlp/services/nlp-entity.service.spec.ts | 2 +- api/src/utils/generics/base-repository.ts | 2 +- api/src/utils/test/fixtures/nlpvalue.ts | 10 +++++----- 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/api/src/nlp/controllers/nlp-entity.controller.ts b/api/src/nlp/controllers/nlp-entity.controller.ts index 9f1f2c94..d0693c87 100644 --- a/api/src/nlp/controllers/nlp-entity.controller.ts +++ b/api/src/nlp/controllers/nlp-entity.controller.ts @@ -223,20 +223,6 @@ export class NlpEntityController extends BaseController< throw new BadRequestException('No IDs provided for deletion.'); } - const { count: builtinNlpEntitiesCount } = await this.filterCount({ - _id: { $in: ids }, - builtin: true, - }); - - if (builtinNlpEntitiesCount > 0) { - this.logger.warn( - `Unable to delete NLP entities with provided IDs: ${ids}`, - ); - throw new MethodNotAllowedException( - 'Deletion failed: Selection includes built-in NLP entities that are protected', - ); - } - const deleteResult = await this.nlpEntityService.deleteMany({ _id: { $in: ids }, }); diff --git a/api/src/nlp/controllers/nlp-value.controller.spec.ts b/api/src/nlp/controllers/nlp-value.controller.spec.ts index 19e80937..981f61c4 100644 --- a/api/src/nlp/controllers/nlp-value.controller.spec.ts +++ b/api/src/nlp/controllers/nlp-value.controller.spec.ts @@ -163,7 +163,7 @@ describe('NlpValueController', () => { entity: intentNlpEntity!.id, value: 'updated', expressions: [], - builtin: true, + builtin: false, doc: '', }; const result = await nlpValueController.updateOne( @@ -191,7 +191,6 @@ describe('NlpValueController', () => { describe('deleteMany', () => { it('should delete multiple nlp values', async () => { const valuesToDelete = [positiveValue!.id, negativeValue!.id]; - const result = await nlpValueController.deleteMany(valuesToDelete); expect(result.deletedCount).toEqual(valuesToDelete.length); diff --git a/api/src/nlp/services/nlp-entity.service.spec.ts b/api/src/nlp/services/nlp-entity.service.spec.ts index 91858e10..61eac63a 100644 --- a/api/src/nlp/services/nlp-entity.service.spec.ts +++ b/api/src/nlp/services/nlp-entity.service.spec.ts @@ -232,7 +232,7 @@ describe('NlpEntityService', () => { { value: 'jhon', expressions: ['john', 'joohn', 'jhonny'], - builtin: true, + builtin: false, doc: '', }, ], diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 16672835..3094d490 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -561,7 +561,7 @@ export abstract class BaseRepository< } async deleteMany(criteria: TFilterQuery): Promise { - return await this.model.deleteMany(criteria); + return await this.model.deleteMany({ ...criteria, builtin: { $ne: true } }); } async preCreateValidate( diff --git a/api/src/utils/test/fixtures/nlpvalue.ts b/api/src/utils/test/fixtures/nlpvalue.ts index d83a582a..92937125 100644 --- a/api/src/utils/test/fixtures/nlpvalue.ts +++ b/api/src/utils/test/fixtures/nlpvalue.ts @@ -18,35 +18,35 @@ export const nlpValueFixtures: NlpValueCreateDto[] = [ entity: '0', value: 'positive', expressions: [], - builtin: true, + builtin: false, doc: '', }, { entity: '0', value: 'negative', expressions: [], - builtin: true, + builtin: false, doc: '', }, { entity: '1', value: 'jhon', expressions: ['john', 'joohn', 'jhonny'], - builtin: true, + builtin: false, doc: '', }, { entity: '0', value: 'greeting', expressions: ['heello', 'Hello', 'hi', 'heyy'], - builtin: true, + builtin: false, doc: '', }, { entity: '0', value: 'goodbye', expressions: ['bye', 'bye bye'], - builtin: true, + builtin: false, doc: '', }, { From 3ff2b97221a14268e3c429def235321f004e9bd8 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 23 May 2025 15:56:08 +0100 Subject: [PATCH 09/53] fix(api): add deleteOne builtin protection --- api/src/utils/generics/base-repository.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 3094d490..ccfabb09 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -555,8 +555,10 @@ export abstract class BaseRepository< } async deleteOne(criteria: string | TFilterQuery): Promise { + const filter = typeof criteria === 'string' ? { _id: criteria } : criteria; + return await this.model - .deleteOne(typeof criteria === 'string' ? { _id: criteria } : criteria) + .deleteOne({ ...filter, builtin: { $ne: true } }) .exec(); } From e0ff78ac71e77f9d276c0ccc487b36b58cacd6eb Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 23 May 2025 15:58:05 +0100 Subject: [PATCH 10/53] fix(api): update unit tests --- .../user/repositories/permission.repository.spec.ts | 1 + api/src/utils/generics/base-repository.spec.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/api/src/user/repositories/permission.repository.spec.ts b/api/src/user/repositories/permission.repository.spec.ts index 5a32ad19..8e5ffbe2 100644 --- a/api/src/user/repositories/permission.repository.spec.ts +++ b/api/src/user/repositories/permission.repository.spec.ts @@ -138,6 +138,7 @@ describe('PermissionRepository', () => { expect(permissionModel.deleteOne).toHaveBeenCalledWith({ _id: permissionToDelete.id, + builtin: { $ne: true }, }); expect(result).toEqual({ diff --git a/api/src/utils/generics/base-repository.spec.ts b/api/src/utils/generics/base-repository.spec.ts index a11a1ae4..57cdd65b 100644 --- a/api/src/utils/generics/base-repository.spec.ts +++ b/api/src/utils/generics/base-repository.spec.ts @@ -306,6 +306,7 @@ describe('BaseRepository', () => { expect(dummyModel.deleteOne).toHaveBeenCalledWith({ _id: createdId, + builtin: { $ne: true }, }); expect(result).toEqualPayload({ acknowledged: true, deletedCount: 1 }); }); @@ -318,12 +319,13 @@ describe('BaseRepository', () => { expect(dummyModel.deleteOne).toHaveBeenCalledWith({ dummy: 'dummy test 2', + builtin: { $ne: true }, }); expect(result).toEqualPayload({ acknowledged: true, deletedCount: 1 }); }); it('should call lifecycle hooks appropriately when deleting by id', async () => { - const criteria = createdId; + jest.spyOn(dummyModel, 'deleteOne'); // Spies for lifecycle hooks const spyBeforeDelete = jest @@ -333,7 +335,12 @@ describe('BaseRepository', () => { .spyOn(dummyRepository, 'postDelete') .mockResolvedValue(); - await dummyRepository.deleteOne(criteria); + await dummyRepository.deleteOne(createdId); + + expect(dummyModel.deleteOne).toHaveBeenCalledWith({ + _id: createdId, + builtin: { $ne: true }, + }); // Verifying that lifecycle hooks are called with correct parameters expect(spyBeforeDelete).toHaveBeenCalledTimes(1); @@ -341,6 +348,7 @@ describe('BaseRepository', () => { expect.objectContaining({ $useProjection: true }), { _id: new Types.ObjectId(createdId), + builtin: { $ne: true }, }, ); expect(spyAfterDelete).toHaveBeenCalledWith( From bc76e53a6b3787dd080daebac35b7d8d2f38e1e5 Mon Sep 17 00:00:00 2001 From: medchedli Date: Sat, 24 May 2025 21:03:15 +0100 Subject: [PATCH 11/53] fix: update version to 2.2.8 in package-lock.json --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77aaa62a..dc712dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hexabot", - "version": "2.2.7", + "version": "2.2.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hexabot", - "version": "2.2.7", + "version": "2.2.8", "license": "AGPL-3.0-only", "workspaces": [ "frontend", From 1400e66702392ccfcd69435b6555cc5e9afb9126 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 25 May 2025 22:13:04 +0100 Subject: [PATCH 12/53] fix(frontend): rename display_mode field --- .../visual-editor/form/ListMessageForm.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/visual-editor/form/ListMessageForm.tsx b/frontend/src/components/visual-editor/form/ListMessageForm.tsx index ac5989b8..0fafb7ea 100644 --- a/frontend/src/components/visual-editor/form/ListMessageForm.tsx +++ b/frontend/src/components/visual-editor/form/ListMessageForm.tsx @@ -47,7 +47,7 @@ const ListMessageForm = () => { formState: { errors }, } = useFormContext(); const contentTypeId = watch("options.content.entity"); - const displayMode = watch("options.content.display_mode"); + const displayMode = watch("options.content.display"); const { data: contentType } = useGet(contentTypeId, { entity: EntityType.CONTENT_TYPE, }); @@ -61,10 +61,9 @@ const ListMessageForm = () => { {t("label.display_mode")} ( {[ @@ -75,10 +74,7 @@ const ListMessageForm = () => { key={display} value={display} control={ - + } label={t(`label.${display}`)} /> From 4981f723c5efbdcb0962a6fb4636304c9a0a4d6f Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 25 May 2025 22:29:58 +0100 Subject: [PATCH 13/53] fix(frontend): add uid for upload components --- .../src/app-components/attachment/AttachmentUploader.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/app-components/attachment/AttachmentUploader.tsx b/frontend/src/app-components/attachment/AttachmentUploader.tsx index fcd3752b..ef5eb377 100644 --- a/frontend/src/app-components/attachment/AttachmentUploader.tsx +++ b/frontend/src/app-components/attachment/AttachmentUploader.tsx @@ -17,7 +17,7 @@ import { styled, Typography, } from "@mui/material"; -import { ChangeEvent, DragEvent, FC, useState } from "react"; +import { ChangeEvent, DragEvent, FC, useId, useState } from "react"; import { useUpload } from "@/hooks/crud/useUpload"; import { useDialogs } from "@/hooks/useDialogs"; @@ -88,6 +88,7 @@ const AttachmentUploader: FC = ({ const [attachment, setAttachment] = useState( undefined, ); + const uid = useId(); const { t } = useTranslate(); const dialogs = useDialogs(); const [isDragOver, setIsDragOver] = useState(false); @@ -154,11 +155,11 @@ const AttachmentUploader: FC = ({ setIsDragOver(true)} onMouseLeave={() => setIsDragOver(false)} From ff716ab47b06013a184d736d28b17d7be9ad9570 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 26 May 2025 08:26:27 +0100 Subject: [PATCH 14/53] fix(api): add missing field to NlpEntityUpdateDto --- api/src/nlp/dto/nlp-entity.dto.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/nlp/dto/nlp-entity.dto.ts b/api/src/nlp/dto/nlp-entity.dto.ts index c986a707..c49e1fc3 100644 --- a/api/src/nlp/dto/nlp-entity.dto.ts +++ b/api/src/nlp/dto/nlp-entity.dto.ts @@ -63,6 +63,14 @@ export class NlpEntityCreateDto { } export class NlpEntityUpdateDto { + @ApiPropertyOptional({ description: 'Name of the nlp entity', type: String }) + @Matches(/^[a-zA-Z0-9_]+$/, { + message: 'Only alphanumeric characters and underscores are allowed.', + }) + @IsString() + @IsOptional() + name?: string; + @ApiPropertyOptional({ type: String }) @IsString() @IsOptional() From fb2fb691fe0b17b76c63c19d77d5e84a4c4482db Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 26 May 2025 18:21:06 +0100 Subject: [PATCH 15/53] fix(frontend): apply feedback --- frontend/public/locales/en/translation.json | 11 +++++++---- frontend/public/locales/fr/translation.json | 9 ++++++--- frontend/src/app-components/auth/Login.tsx | 2 +- frontend/src/app-components/auth/Register.tsx | 2 +- frontend/src/app-components/auth/ResetPassword.tsx | 4 ++-- .../src/components/nlp/components/NlpValueForm.tsx | 2 +- frontend/src/components/profile/profile.tsx | 2 +- frontend/src/components/settings/SettingInput.tsx | 2 +- frontend/src/components/users/EditUserForm.tsx | 2 +- 9 files changed, 21 insertions(+), 15 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index e3ed3a75..703cf684 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -123,7 +123,7 @@ "video_error": "Video not found", "missing_fields_error": "Please make sure that all required fields are filled", "weight_required_error": "Weight is required or invalid", - "weight_positive_number_error": "Weight must be a strictly positive number" + "weight_positive_number_error": "Weight must be a strictly positive number" }, "menu": { "terms": "Terms of Use", @@ -490,7 +490,9 @@ "original_text": "Original Text", "inputs": "Inputs", "outputs": "Outputs", - "any": "- Any -" + "any": "- Any -", + "full_name": "First and last name", + "password": "Password" }, "placeholder": { "your_username": "Your username", @@ -498,7 +500,6 @@ "your_password": "Your password", "username": "Username", "email": "E-mail address", - "full_name": "First and last name", "password": "Password", "password2": "Confirm your password", "timezone": "Timezone", @@ -527,7 +528,9 @@ "end_date": "End Date", "nlp_value": "Value", "type_message_here": "Type message here ....", - "mark_as_default": "By Default" + "mark_as_default": "By Default", + "pattern": "Pattern", + "full_name": "First and last name" }, "button": { "login": "Sign In", diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index ad69c256..783f03dc 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -491,7 +491,9 @@ "original_text": "Texte par défaut", "inputs": "Ports d'entrée", "outputs": "Ports de sortie", - "any": "- Toutes -" + "any": "- Toutes -", + "full_name": "Nom et Prénom", + "password": "Mot de passe" }, "placeholder": { "your_username": "Votre nom d'utilisateur", @@ -499,7 +501,6 @@ "your_password": "Votre mot de passe", "username": "Nom d'utilisateur", "email": "Adresse e-mail", - "full_name": "Nom et Prénom", "password": "Mot de passe", "password2": "Confirmez votre mot de passe", "timezone": "Fuseau horaire", @@ -528,7 +529,9 @@ "end_date": "Date de fin", "nlp_value": "Valeur", "type_message_here": "Ecrivez quelque chose ici ....", - "mark_as_default": "Par Défaut" + "mark_as_default": "Par Défaut", + "pattern": "Motif", + "full_name": "Nom et Prénom" }, "button": { "login": "Se connecter", diff --git a/frontend/src/app-components/auth/Login.tsx b/frontend/src/app-components/auth/Login.tsx index 4bc53960..a1e95f4e 100755 --- a/frontend/src/app-components/auth/Login.tsx +++ b/frontend/src/app-components/auth/Login.tsx @@ -110,7 +110,7 @@ export const Login = () => { /> { { > = ({ helperText={errors.metadata?.pattern?.message} error={!!errors.metadata?.pattern} label={t("label.regex")} - placeholder={t("label.pattern")} + placeholder={t("placeholder.pattern")} flags={["i"]} /> diff --git a/frontend/src/components/profile/profile.tsx b/frontend/src/components/profile/profile.tsx index 5d4df1f1..0783d087 100644 --- a/frontend/src/components/profile/profile.tsx +++ b/frontend/src/components/profile/profile.tsx @@ -177,7 +177,7 @@ export const ProfileForm: FC = ({ user }) => { = ({ entity={EntityType.BLOCK} format={Format.BASIC} labelKey="name" - label={t("help.fallback_message")} + label={t("label.fallback_message")} helperText={t("help.fallback_message")} multiple={false} onChange={(_e, selected, ..._) => onChange(selected?.id || "")} diff --git a/frontend/src/components/users/EditUserForm.tsx b/frontend/src/components/users/EditUserForm.tsx index 59b26556..b3b1c456 100644 --- a/frontend/src/components/users/EditUserForm.tsx +++ b/frontend/src/components/users/EditUserForm.tsx @@ -76,7 +76,7 @@ export const EditUserForm: FC> = ({ Date: Tue, 27 May 2025 18:14:44 +0100 Subject: [PATCH 16/53] feat: implement OnApplicationBootstrap for LlmNluHelper to build prompts during initialization --- .../helpers/llm-nlu/index.helper.ts | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/api/src/extensions/helpers/llm-nlu/index.helper.ts b/api/src/extensions/helpers/llm-nlu/index.helper.ts index 5c7c9b2f..d6fff4b2 100644 --- a/api/src/extensions/helpers/llm-nlu/index.helper.ts +++ b/api/src/extensions/helpers/llm-nlu/index.helper.ts @@ -6,7 +6,11 @@ * 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 { Injectable, OnModuleInit } from '@nestjs/common'; +import { + Injectable, + OnApplicationBootstrap, + OnModuleInit, +} from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import Handlebars from 'handlebars'; @@ -24,7 +28,7 @@ import { LLM_NLU_HELPER_NAME } from './settings'; @Injectable() export default class LlmNluHelper extends BaseNlpHelper - implements OnModuleInit + implements OnModuleInit, OnApplicationBootstrap { private languageClassifierPrompt: string; @@ -50,13 +54,17 @@ export default class LlmNluHelper @OnEvent('hook:language:*') @OnEvent('hook:llm_nlu_helper:language_classifier_prompt_template') async buildLanguageClassifierPrompt() { - const settings = await this.getSettings(); - if (settings) { + try { + const settings = await this.getSettings(); const languages = await this.languageService.findAll(); const delegate = Handlebars.compile( settings.language_classifier_prompt_template, ); this.languageClassifierPrompt = delegate({ languages }); + } catch (error) { + this.logger.warn( + 'Settings for LLM NLU helper not found or invalid, language classifier prompt will not be built.', + ); } } @@ -64,8 +72,8 @@ export default class LlmNluHelper @OnEvent('hook:nlpValue:*') @OnEvent('hook:llm_nlu_helper:trait_classifier_prompt_template') async buildClassifiersPrompt() { - const settings = await this.getSettings(); - if (settings) { + try { + const settings = await this.getSettings(); const traitEntities = await this.nlpEntityService.findAndPopulate({ lookups: 'trait', }); @@ -75,14 +83,29 @@ export default class LlmNluHelper entity, }), })); + } catch (error) { + this.logger.warn( + 'Settings for LLM NLU helper not found or invalid, trait classifier prompts will not be built.', + ); } } async onModuleInit() { super.onModuleInit(); + // Add any additional initialization logic here if needed + } - await this.buildLanguageClassifierPrompt(); - await this.buildClassifiersPrompt(); + async onApplicationBootstrap() { + try { + this.logger.log('Initializing LLM NLU helper, building prompts...'); + // Build prompts for language and trait classifiers + // This is done on application bootstrap to ensure that the settings are loaded + // and the prompts are built before any requests are made to the helper. + await this.buildLanguageClassifierPrompt(); + await this.buildClassifiersPrompt(); + } catch (error) { + this.logger.error('Unable to initialize LLM NLU helper', error); + } } async predict(text: string): Promise { From 4cb3229f1b5cc41c570ee1306d54c26e71a89cad Mon Sep 17 00:00:00 2001 From: medchedli Date: Tue, 27 May 2025 20:03:34 +0100 Subject: [PATCH 17/53] feat: add default NLU penalty factor setting --- api/src/chat/services/block.service.spec.ts | 26 ++++++++++++++++++- api/src/chat/services/block.service.ts | 24 ++++++++++++++--- api/src/setting/seeds/setting.seed-model.ts | 22 ++++++++++++---- api/src/utils/constants/nlp.ts | 9 +++++++ .../public/locales/en/chatbot_settings.json | 6 +++-- .../public/locales/fr/chatbot_settings.json | 6 +++-- 6 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 api/src/utils/constants/nlp.ts diff --git a/api/src/chat/services/block.service.spec.ts b/api/src/chat/services/block.service.spec.ts index b945cacc..f4d45440 100644 --- a/api/src/chat/services/block.service.spec.ts +++ b/api/src/chat/services/block.service.spec.ts @@ -47,6 +47,7 @@ import { NlpValueService } from '@/nlp/services/nlp-value.service'; import { NlpService } from '@/nlp/services/nlp.service'; import { PluginService } from '@/plugins/plugins.service'; import { SettingService } from '@/setting/services/setting.service'; +import { FALLBACK_DEFAULT_NLU_PENALTY_FACTOR } from '@/utils/constants/nlp'; import { blockFixtures, installBlockFixtures, @@ -196,6 +197,7 @@ describe('BlockService', () => { })), getSettings: jest.fn(() => ({ contact: { company_name: 'Your company name' }, + chatbot_settings: { default_nlu_penalty_factor: 0.95 }, })), }, }, @@ -467,9 +469,11 @@ describe('BlockService', () => { blockService, 'calculateNluPatternMatchScore', ); + const bestBlock = blockService.matchBestNLP( blocks, mockNlpGreetingNameEntities, + FALLBACK_DEFAULT_NLU_PENALTY_FACTOR, ); // Ensure calculateBlockScore was called at least once for each block @@ -509,7 +513,11 @@ describe('BlockService', () => { blockService, 'calculateNluPatternMatchScore', ); - const bestBlock = blockService.matchBestNLP(blocks, nlp); + const bestBlock = blockService.matchBestNLP( + blocks, + nlp, + FALLBACK_DEFAULT_NLU_PENALTY_FACTOR, + ); // Ensure calculateBlockScore was called at least once for each block expect(calculateBlockScoreSpy).toHaveBeenCalledTimes(3); // Called for each block @@ -530,6 +538,7 @@ describe('BlockService', () => { const bestBlock = blockService.matchBestNLP( blocks, mockNlpGreetingNameEntities, + FALLBACK_DEFAULT_NLU_PENALTY_FACTOR, ); // Assert that undefined is returned when no blocks are available @@ -542,6 +551,7 @@ describe('BlockService', () => { const matchingScore = blockService.calculateNluPatternMatchScore( mockNlpGreetingNamePatterns, mockNlpGreetingNameEntities, + FALLBACK_DEFAULT_NLU_PENALTY_FACTOR, ); expect(matchingScore).toBeGreaterThan(0); @@ -551,15 +561,29 @@ describe('BlockService', () => { const scoreWithoutPenalty = blockService.calculateNluPatternMatchScore( mockNlpGreetingNamePatterns, mockNlpGreetingNameEntities, + FALLBACK_DEFAULT_NLU_PENALTY_FACTOR, ); const scoreWithPenalty = blockService.calculateNluPatternMatchScore( mockNlpGreetingAnyNamePatterns, mockNlpGreetingNameEntities, + FALLBACK_DEFAULT_NLU_PENALTY_FACTOR, ); expect(scoreWithoutPenalty).toBeGreaterThan(scoreWithPenalty); }); + + it('should handle invalid case for penalty factor values', async () => { + // Test with invalid penalty (should use fallback) + const scoreWithInvalidPenalty = + blockService.calculateNluPatternMatchScore( + mockNlpGreetingAnyNamePatterns, + mockNlpGreetingNameEntities, + -1, + ); + + expect(scoreWithInvalidPenalty).toBeGreaterThan(0); // Should use fallback value + }); }); describe('matchPayload', () => { diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index 47bc9cc1..d5a20d7a 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -20,6 +20,7 @@ import { NlpService } from '@/nlp/services/nlp.service'; import { PluginService } from '@/plugins/plugins.service'; import { PluginType } from '@/plugins/types'; import { SettingService } from '@/setting/services/setting.service'; +import { FALLBACK_DEFAULT_NLU_PENALTY_FACTOR } from '@/utils/constants/nlp'; import { BaseService } from '@/utils/generics/base-service'; import { getRandomElement } from '@/utils/helpers/safeRandom'; @@ -180,8 +181,23 @@ export class BlockService extends BaseService< const scoredEntities = await this.nlpService.computePredictionScore(nlp); + const settings = await this.settingService.getSettings(); + let penaltyFactor = + settings.chatbot_settings?.default_nlu_penalty_factor; + if (!penaltyFactor) { + this.logger.warn( + 'Using fallback NLU penalty factor value: %s', + FALLBACK_DEFAULT_NLU_PENALTY_FACTOR, + ); + penaltyFactor = FALLBACK_DEFAULT_NLU_PENALTY_FACTOR; + } + if (scoredEntities.entities.length > 0) { - block = this.matchBestNLP(filteredBlocks, scoredEntities); + block = this.matchBestNLP( + filteredBlocks, + scoredEntities, + penaltyFactor, + ); } } } @@ -351,6 +367,7 @@ export class BlockService extends BaseService< matchBestNLP( blocks: B[], scoredEntities: NLU.ScoredEntities, + penaltyFactor: number, ): B | undefined { const bestMatch = blocks.reduce( (bestMatch, block) => { @@ -365,10 +382,10 @@ export class BlockService extends BaseService< const score = this.calculateNluPatternMatchScore( patterns, scoredEntities, + penaltyFactor, ); return Math.max(maxScore, score); }, 0); - return score > bestMatch.score ? { block, score } : bestMatch; }, { block: undefined, score: 0 }, @@ -390,14 +407,13 @@ export class BlockService extends BaseService< * * @param patterns - A list of patterns to evaluate against the NLU prediction. * @param prediction - The scored entities resulting from NLU inference. - * @param [penaltyFactor=0.95] - Optional penalty factor to apply for generic matches (default is 0.95). * * @returns The total aggregated match score based on matched patterns and their computed scores. */ calculateNluPatternMatchScore( patterns: NlpPattern[], prediction: NLU.ScoredEntities, - penaltyFactor = 0.95, + penaltyFactor: number, ): number { if (!patterns.length || !prediction.entities.length) { return 0; diff --git a/api/src/setting/seeds/setting.seed-model.ts b/api/src/setting/seeds/setting.seed-model.ts index 27cf5785..c65d9be3 100644 --- a/api/src/setting/seeds/setting.seed-model.ts +++ b/api/src/setting/seeds/setting.seed-model.ts @@ -24,6 +24,18 @@ export const DEFAULT_SETTINGS = [ }, weight: 1, }, + { + group: 'chatbot_settings', + label: 'default_nlu_penalty_factor', + value: 0.95, + type: SettingType.number, + config: { + min: 0, + max: 1, + step: 0.01, + }, + weight: 2, + }, { group: 'chatbot_settings', label: 'default_llm_helper', @@ -36,7 +48,7 @@ export const DEFAULT_SETTINGS = [ idKey: 'name', labelKey: 'name', }, - weight: 2, + weight: 3, }, { group: 'chatbot_settings', @@ -50,14 +62,14 @@ export const DEFAULT_SETTINGS = [ idKey: 'name', labelKey: 'name', }, - weight: 3, + weight: 4, }, { group: 'chatbot_settings', label: 'global_fallback', value: true, type: SettingType.checkbox, - weight: 4, + weight: 5, }, { group: 'chatbot_settings', @@ -72,7 +84,7 @@ export const DEFAULT_SETTINGS = [ idKey: 'id', labelKey: 'name', }, - weight: 5, + weight: 6, }, { group: 'chatbot_settings', @@ -82,7 +94,7 @@ export const DEFAULT_SETTINGS = [ "I'm really sorry but i don't quite understand what you are saying :(", ] as string[], type: SettingType.multiple_text, - weight: 6, + weight: 7, translatable: true, }, { diff --git a/api/src/utils/constants/nlp.ts b/api/src/utils/constants/nlp.ts new file mode 100644 index 00000000..fb281d5d --- /dev/null +++ b/api/src/utils/constants/nlp.ts @@ -0,0 +1,9 @@ +/* + * 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). + */ + +export const FALLBACK_DEFAULT_NLU_PENALTY_FACTOR = 0.95; diff --git a/frontend/public/locales/en/chatbot_settings.json b/frontend/public/locales/en/chatbot_settings.json index 170af944..bde69e7b 100644 --- a/frontend/public/locales/en/chatbot_settings.json +++ b/frontend/public/locales/en/chatbot_settings.json @@ -8,13 +8,15 @@ "fallback_block": "Fallback Block", "default_nlu_helper": "Default NLU Helper", "default_llm_helper": "Default LLM Helper", - "default_storage_helper": "Default Storage Helper" + "default_storage_helper": "Default Storage Helper", + "default_nlu_penalty_factor": "NLU Penalty Factor" }, "help": { "global_fallback": "Global fallback allows you to send custom messages when user entry does not match any of the block messages.", "fallback_message": "If no fallback block is selected, then one of these messages will be sent.", "default_nlu_helper": "The NLU helper is responsible for processing and understanding user inputs, including tasks like intent prediction, language detection, and entity recognition.", "default_llm_helper": "The LLM helper leverages advanced generative AI to perform tasks such as text generation, chat completion, and complex query responses.", - "default_storage_helper": "The storage helper defines where to store attachment files. By default, the default local storage helper stores them locally, but you can choose to use Minio or any other storage solution." + "default_storage_helper": "The storage helper defines where to store attachment files. By default, the default local storage helper stores them locally, but you can choose to use Minio or any other storage solution.", + "default_nlu_penalty_factor": "The NLU penalty factor is a coefficient (between 0 and 1) applied exclusively to NLU-based entity matching. It reduces the score contribution of patterns that match broadly (e.g. using wildcard values like Any) rather than specific entity values. This helps the engine prioritize blocks triggered by more precise NLU matches, without affecting other matching strategies such as text, regex, or interaction triggers." } } diff --git a/frontend/public/locales/fr/chatbot_settings.json b/frontend/public/locales/fr/chatbot_settings.json index 4b1ed420..9d44b07c 100644 --- a/frontend/public/locales/fr/chatbot_settings.json +++ b/frontend/public/locales/fr/chatbot_settings.json @@ -8,13 +8,15 @@ "fallback_block": "Bloc de secours", "default_nlu_helper": "Utilitaire NLU par défaut", "default_llm_helper": "Utilitaire LLM par défaut", - "default_storage_helper": "Utilitaire de stockage par défaut" + "default_storage_helper": "Utilitaire de stockage par défaut", + "default_nlu_penalty_factor": "Facteur de pénalité NLU" }, "help": { "global_fallback": "La réponse de secours globale vous permet d'envoyer des messages personnalisés lorsque l'entrée de l'utilisateur ne correspond à aucun des messages des blocs.", "fallback_message": "Si aucun bloc de secours n'est sélectionné, l'un de ces messages sera envoyé.", "default_nlu_helper": "Utilitaire du traitement et de la compréhension des entrées des utilisateurs, incluant des tâches telles que la prédiction d'intention, la détection de langue et la reconnaissance d'entités.", "default_llm_helper": "Utilitaire responsable de l'intelligence artificielle générative avancée pour effectuer des tâches telles que la génération de texte, la complétion de chat et les réponses à des requêtes complexes.", - "default_storage_helper": "Utilitaire de stockage définit l'emplacement où stocker les fichiers joints. Par défaut, le stockage local les conserve localement, mais vous pouvez choisir d'utiliser Minio ou toute autre solution de stockage." + "default_storage_helper": "Utilitaire de stockage définit l'emplacement où stocker les fichiers joints. Par défaut, le stockage local les conserve localement, mais vous pouvez choisir d'utiliser Minio ou toute autre solution de stockage.", + "default_nlu_penalty_factor": "Le facteur de pénalité NLU est un coefficient (entre 0 et 1) appliqué exclusivement aux correspondances d'entités basées sur NLU. Il réduit la contribution au score des motifs qui correspondent de manière générale (par exemple, en utilisant des valeurs génériques comme Any) plutôt que des valeurs d'entité spécifiques. Cela permet au chatbot de donner la priorité aux blocs déclenchés par des correspondances NLU plus précises, sans affecter d'autres stratégies de correspondance telles que le texte, les expressions regex ou les déclencheurs d'interaction." } } From 0c31d3047eed6fbfda5894778a8df7726d1410e2 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Wed, 28 May 2025 16:26:32 +0100 Subject: [PATCH 18/53] fix: revert to older version --- .../inbox/components/ConversationsList.tsx | 6 +-- frontend/src/components/inbox/index.tsx | 3 +- frontend/src/hooks/useSearch.tsx | 41 ++++--------------- frontend/src/utils/URL.ts | 11 ----- 4 files changed, 9 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/inbox/components/ConversationsList.tsx b/frontend/src/components/inbox/components/ConversationsList.tsx index 3a1b1c8b..2d71b614 100644 --- a/frontend/src/components/inbox/components/ConversationsList.tsx +++ b/frontend/src/components/inbox/components/ConversationsList.tsx @@ -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"; @@ -75,10 +74,7 @@ export const SubscribersList = (props: { { chat.setSubscriberId(subscriber.id); - push({ - pathname: `/${RouterType.INBOX}/subscribers/${subscriber.id}`, - query: extractQueryParamsUrl(window.location.href), - }); + push(`/${RouterType.INBOX}/subscribers/${subscriber.id}`); }} className="changeColor" key={subscriber.id} diff --git a/frontend/src/components/inbox/index.tsx b/frontend/src/components/inbox/index.tsx index e57e8192..a34d6d62 100644 --- a/frontend/src/components/inbox/index.tsx +++ b/frontend/src/components/inbox/index.tsx @@ -26,7 +26,7 @@ import { AssignedTo } from "./types"; export const Inbox = () => { const { t } = useTranslate(); - const { onSearch, searchPayload, searchText } = useSearch({ + const { onSearch, searchPayload } = useSearch({ $or: ["first_name", "last_name"], }); const [channels, setChannels] = useState([]); @@ -48,7 +48,6 @@ export const Inbox = () => { onSearch("")} className="changeColor" onChange={(v) => onSearch(v)} diff --git a/frontend/src/hooks/useSearch.tsx b/frontend/src/hooks/useSearch.tsx index ac4e2605..34c6ab25 100644 --- a/frontend/src/hooks/useSearch.tsx +++ b/frontend/src/hooks/useSearch.tsx @@ -7,8 +7,7 @@ */ import { debounce } from "@mui/material"; -import { useRouter } from "next/router"; -import { ChangeEvent, useCallback, useEffect, useState } from "react"; +import { ChangeEvent, useState } from "react"; import { TBuildInitialParamProps, @@ -53,38 +52,13 @@ const buildNeqInitialParams = ({ ); export const useSearch = (params: TParamItem) => { - const router = useRouter(); - const [searchText, setSearchText] = useState( - (router.query.search as string) || "", + const [searchText, setSearchText] = useState(""); + const onSearch = debounce( + (e: ChangeEvent | string) => { + setSearchText(typeof e === "string" ? e : e.target.value); + }, + 300, ); - - 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 | string, - ) => { - const newSearchText = typeof e === "string" ? e : e.target.value; - - setSearchText(newSearchText); - updateQueryParams(newSearchText); - }; const { $eq: eqInitialParams, $iLike: iLikeParams, @@ -93,7 +67,6 @@ export const useSearch = (params: TParamItem) => { } = params; return { - searchText, onSearch, searchPayload: { where: { diff --git a/frontend/src/utils/URL.ts b/frontend/src/utils/URL.ts index 4656c4ee..bbb01a14 100644 --- a/frontend/src/utils/URL.ts +++ b/frontend/src/utils/URL.ts @@ -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); -}; From 0f8f0dd27fae2edf89efd612a6a29cf5093cc48b Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Wed, 28 May 2025 17:06:23 +0100 Subject: [PATCH 19/53] fix: move debounce to text input and enhance --- .../app-components/inputs/FilterTextfield.tsx | 61 +++++++++++++++++-- frontend/src/components/inbox/index.tsx | 10 +-- frontend/src/hooks/useSearch.tsx | 13 ++-- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/frontend/src/app-components/inputs/FilterTextfield.tsx b/frontend/src/app-components/inputs/FilterTextfield.tsx index cd58df06..bbd3af20 100644 --- a/frontend/src/app-components/inputs/FilterTextfield.tsx +++ b/frontend/src/app-components/inputs/FilterTextfield.tsx @@ -1,31 +1,80 @@ /* - * 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, useMemo, useState } 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 { + 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 [inputValue, setInputValue] = useState(defaultValue); + const debouncedSearch = useMemo( + () => + debounce((value: string) => { + onSearch?.(value); + }, delay), + [onSearch, delay], + ); + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + + setInputValue(value); + debouncedSearch(value); + }, + [debouncedSearch], + ); + const handleClear = useCallback(() => { + setInputValue(""); + debouncedSearch(""); + }, [debouncedSearch]); - //TODO: replace the native delete text button by a styled custom button return ( , + endAdornment: clearable && ( + + + + + + ), }} placeholder={t("placeholder.keywords")} {...props} + value={inputValue} + onChange={handleChange} /> ); }; diff --git a/frontend/src/components/inbox/index.tsx b/frontend/src/components/inbox/index.tsx index a34d6d62..598b7461 100644 --- a/frontend/src/components/inbox/index.tsx +++ b/frontend/src/components/inbox/index.tsx @@ -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"; @@ -46,13 +47,14 @@ export const Inbox = () => { - - + + {/* onSearch("")} className="changeColor" onChange={(v) => onSearch(v)} placeholder="Search..." - /> + /> */} ({ export const useSearch = (params: TParamItem) => { const [searchText, setSearchText] = useState(""); - const onSearch = debounce( - (e: ChangeEvent | string) => { - setSearchText(typeof e === "string" ? e : e.target.value); - }, - 300, - ); const { $eq: eqInitialParams, $iLike: iLikeParams, @@ -67,7 +60,11 @@ export const useSearch = (params: TParamItem) => { } = params; return { - onSearch, + onSearch: ( + e: ChangeEvent | string, + ) => { + setSearchText(typeof e === "string" ? e : e.target.value); + }, searchPayload: { where: { ...buildEqInitialParams({ initialParams: eqInitialParams }), From 6b0ad29ee87648fe6a4c0cc3dd9db7f8b39574d5 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Wed, 28 May 2025 17:06:54 +0100 Subject: [PATCH 20/53] feat: add query param hook --- frontend/src/hooks/useUrlQueryParam.ts | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 frontend/src/hooks/useUrlQueryParam.ts diff --git a/frontend/src/hooks/useUrlQueryParam.ts b/frontend/src/hooks/useUrlQueryParam.ts new file mode 100644 index 00000000..770d7f8c --- /dev/null +++ b/frontend/src/hooks/useUrlQueryParam.ts @@ -0,0 +1,67 @@ +/* + * 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'; + +export const useUrlQueryParam = ( + key: string, + defaultValue: T +): [T, (val: T) => void] => { + const router = useRouter(); + const [value, setValue] = useState(() => { + // 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 JSON.parse(initial as string) 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 = JSON.parse(urlValue as string); + } 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) => { + debugger + setValue(val); + if (!router.isReady) return; + const newQuery = { ...router.query }; + + if (val === defaultValue || val === undefined || val === '') { + delete newQuery[key]; + } else { + newQuery[key] = typeof val === 'string' ? val : JSON.stringify(val); + } + router.push({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true }); + }, [router, key, defaultValue]); + + return [value, updateValue]; +} \ No newline at end of file From 7a4f8a7786932693bdc5f65d2ca4fe65ecebae39 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Wed, 28 May 2025 19:07:46 +0100 Subject: [PATCH 21/53] feat: apply across all pages --- .../attachment/AttachmentUploader.tsx | 2 +- .../app-components/inputs/FilterTextfield.tsx | 30 +++++-- frontend/src/components/categories/index.tsx | 11 ++- .../src/components/content-types/index.tsx | 11 ++- frontend/src/components/contents/index.tsx | 13 ++-- .../src/components/context-vars/index.tsx | 11 ++- frontend/src/components/inbox/index.tsx | 20 ++--- frontend/src/components/labels/index.tsx | 11 ++- frontend/src/components/languages/index.tsx | 11 ++- .../src/components/media-library/index.tsx | 12 ++- .../components/nlp/components/NlpEntity.tsx | 11 ++- .../components/nlp/components/NlpSample.tsx | 18 +++-- .../components/nlp/components/NlpValue.tsx | 16 ++-- frontend/src/components/roles/index.tsx | 13 ++-- frontend/src/components/subscribers/index.tsx | 19 +++-- .../src/components/translations/index.tsx | 11 ++- frontend/src/components/users/index.tsx | 13 ++-- frontend/src/hooks/useSearch.tsx | 26 ++++++- frontend/src/hooks/useUrlQueryParam.ts | 78 +++++++++++++++---- 19 files changed, 236 insertions(+), 101 deletions(-) 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" > - +