From 3322855e1a182b6d4b634305b7359e4c1bb92ba9 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Tue, 25 Feb 2025 16:35:44 +0100 Subject: [PATCH 01/58] feat: support redirection with query params --- frontend/src/contexts/auth.context.tsx | 26 +++++++++----------------- frontend/src/utils/URL.ts | 20 -------------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/frontend/src/contexts/auth.context.tsx b/frontend/src/contexts/auth.context.tsx index 99978f15..04e21c4d 100644 --- a/frontend/src/contexts/auth.context.tsx +++ b/frontend/src/contexts/auth.context.tsx @@ -8,7 +8,7 @@ import getConfig from "next/config"; import { useRouter } from "next/router"; -import { createContext, ReactNode, useEffect, useState } from "react"; +import { createContext, ReactNode } from "react"; import { QueryObserverResult, RefetchOptions, @@ -25,7 +25,6 @@ import { useSubscribeBroadcastChannel } from "@/hooks/useSubscribeBroadcastChann import { useTranslate } from "@/hooks/useTranslate"; import { RouterType } from "@/services/types"; import { IUser } from "@/types/user.types"; -import { getFromQuery } from "@/utils/URL"; export interface AuthContextValue { user: IUser | undefined; @@ -51,10 +50,8 @@ const { publicRuntimeConfig } = getConfig(); export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { const router = useRouter(); - const [search, setSearch] = useState(""); const hasPublicPath = PUBLIC_PATHS.includes(router.pathname); const { i18n } = useTranslate(); - const [isReady, setIsReady] = useState(false); const queryClient = useQueryClient(); const updateLanguage = (lang: string) => { i18n.changeLanguage(lang); @@ -66,11 +63,11 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { }; const authRedirection = async (isAuthenticated: boolean) => { if (isAuthenticated) { - const redirect = getFromQuery({ search, key: "redirect" }); - const nextPage = redirect && decodeURIComponent(redirect); - - if (nextPage?.startsWith("/")) { - await router.push(nextPage); + if ( + router.query.redirect && + router.query.redirect.toString().startsWith("/") + ) { + await router.push(router.query.redirect.toString()); } else if (hasPublicPath) { await router.push(RouterType.HOME); } @@ -109,14 +106,9 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { router.reload(); }); - useEffect(() => { - const search = location.search; - - setSearch(search); - setIsReady(true); - }, []); - - if (!isReady || isLoading) return ; + if (isLoading) { + return ; + } return ( { - try { - const paramsString = search || window.location.search; - const searchParams = new URLSearchParams(paramsString); - const loadCampaign = searchParams.get(key) || defaultValue; - - return loadCampaign; - } catch (e) { - return defaultValue; - } -}; - export const buildURL = (baseUrl: string, relativePath: string): string => { try { return new URL(relativePath).toString(); From 8077263ecf8fa8653aed1cdf866216a8d078b4cd Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Fri, 28 Mar 2025 10:11:24 +0100 Subject: [PATCH 02/58] fix: custom plugins displayed sorted --- .../src/components/visual-editor/CustomBlocks.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/visual-editor/CustomBlocks.tsx b/frontend/src/components/visual-editor/CustomBlocks.tsx index 249cbe78..cf17cabb 100644 --- a/frontend/src/components/visual-editor/CustomBlocks.tsx +++ b/frontend/src/components/visual-editor/CustomBlocks.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. @@ -7,6 +7,7 @@ */ import { Grid } from "@mui/material"; +import { useMemo } from "react"; import PluginIcon from "@/app-components/svg/toolbar/PluginIcon"; import { useFind } from "@/hooks/crud/useFind"; @@ -17,18 +18,22 @@ import { Block, StyledTitle } from "./Aside"; export const CustomBlocks = () => { const { t } = useTranslate(); - const { data: customBlocks } = useFind( + const { data: customBlocks = [] } = useFind( { entity: EntityType.CUSTOM_BLOCK }, { hasCount: false }, ); + const memoizedCustomBlock = useMemo( + () => customBlocks.sort((a, b) => a.id.localeCompare(b.id)), + [customBlocks], + ); - return customBlocks?.length ? ( + return memoizedCustomBlock.length ? ( <> {t("title.custom_blocks")} - {customBlocks?.map((customBlock) => ( + {memoizedCustomBlock.map((customBlock) => ( Date: Wed, 2 Apr 2025 12:08:51 +0100 Subject: [PATCH 03/58] fix: pressist search state inbox --- frontend/src/components/inbox/index.tsx | 5 +-- frontend/src/hooks/useSearch.tsx | 47 +++++++++++++++++++------ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/inbox/index.tsx b/frontend/src/components/inbox/index.tsx index 6a56e84a..e57e8192 100644 --- a/frontend/src/components/inbox/index.tsx +++ b/frontend/src/components/inbox/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. @@ -26,7 +26,7 @@ import { AssignedTo } from "./types"; export const Inbox = () => { const { t } = useTranslate(); - const { onSearch, searchPayload } = useSearch({ + const { onSearch, searchPayload, searchText } = useSearch({ $or: ["first_name", "last_name"], }); const [channels, setChannels] = useState([]); @@ -48,6 +48,7 @@ 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 384ac5e9..ac4e2605 100644 --- a/frontend/src/hooks/useSearch.tsx +++ b/frontend/src/hooks/useSearch.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. @@ -7,12 +7,13 @@ */ import { debounce } from "@mui/material"; -import { ChangeEvent, useState } from "react"; +import { useRouter } from "next/router"; +import { ChangeEvent, useCallback, useEffect, useState } from "react"; import { - TParamItem, - TBuildParamProps, TBuildInitialParamProps, + TBuildParamProps, + TParamItem, } from "@/types/search.types"; const buildOrParams = ({ params, searchText }: TBuildParamProps) => ({ @@ -52,13 +53,38 @@ const buildNeqInitialParams = ({ ); 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 router = useRouter(); + const [searchText, setSearchText] = useState( + (router.query.search as string) || "", ); + + 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, @@ -67,6 +93,7 @@ export const useSearch = (params: TParamItem) => { } = params; return { + searchText, onSearch, searchPayload: { where: { From d96ba082f7abb010f7ca45f422a543c5a9377bb5 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Thu, 3 Apr 2025 10:04:58 +0100 Subject: [PATCH 04/58] fix: apply feedback --- frontend/src/components/visual-editor/CustomBlocks.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/visual-editor/CustomBlocks.tsx b/frontend/src/components/visual-editor/CustomBlocks.tsx index cf17cabb..11304829 100644 --- a/frontend/src/components/visual-editor/CustomBlocks.tsx +++ b/frontend/src/components/visual-editor/CustomBlocks.tsx @@ -22,18 +22,18 @@ export const CustomBlocks = () => { { entity: EntityType.CUSTOM_BLOCK }, { hasCount: false }, ); - const memoizedCustomBlock = useMemo( + const memoizedCustomBlocks = useMemo( () => customBlocks.sort((a, b) => a.id.localeCompare(b.id)), [customBlocks], ); - return memoizedCustomBlock.length ? ( + return memoizedCustomBlocks.length ? ( <> {t("title.custom_blocks")} - {memoizedCustomBlock.map((customBlock) => ( + {memoizedCustomBlocks.map((customBlock) => ( Date: Thu, 3 Apr 2025 16:50:44 +0100 Subject: [PATCH 05/58] fix: remove unused parameter when emitting hook-conversation-end event --- api/src/chat/services/bot.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/src/chat/services/bot.service.ts b/api/src/chat/services/bot.service.ts index 46341b54..bb6c4871 100644 --- a/api/src/chat/services/bot.service.ts +++ b/api/src/chat/services/bot.service.ts @@ -165,7 +165,7 @@ export class BotService { return await this.triggerBlock(event, convo, attachedBlock, fallback); } catch (err) { this.logger.error('Unable to retrieve attached block', err); - this.eventEmitter.emit('hook:conversation:end', convo, true); + this.eventEmitter.emit('hook:conversation:end', convo); } } else if ( Array.isArray(block.nextBlocks) && @@ -200,7 +200,7 @@ export class BotService { 'Block outcome did not match any of the next blocks', convo, ); - this.eventEmitter.emit('hook:conversation:end', convo, false); + this.eventEmitter.emit('hook:conversation:end', convo); } } else { // Conversation continues : Go forward to next blocks @@ -218,11 +218,11 @@ export class BotService { } else { // We need to end the conversation in this case this.logger.debug('No attached/next blocks to execute ...'); - this.eventEmitter.emit('hook:conversation:end', convo, false); + this.eventEmitter.emit('hook:conversation:end', convo); } } catch (err) { this.logger.error('Unable to process/send message.', err); - this.eventEmitter.emit('hook:conversation:end', convo, true); + this.eventEmitter.emit('hook:conversation:end', convo); } } @@ -309,19 +309,19 @@ export class BotService { await this.triggerBlock(event, updatedConversation, next, fallback); } catch (err) { this.logger.error('Unable to store context data!', err); - return this.eventEmitter.emit('hook:conversation:end', convo, true); + return this.eventEmitter.emit('hook:conversation:end', convo); } return true; } else { // Conversation is still active, but there's no matching block to call next // We'll end the conversation but this message is probably lost in time and space. this.logger.debug('No matching block found to call next ', convo.id); - this.eventEmitter.emit('hook:conversation:end', convo, false); + this.eventEmitter.emit('hook:conversation:end', convo); return false; } } catch (err) { this.logger.error('Unable to populate the next blocks!', err); - this.eventEmitter.emit('hook:conversation:end', convo, true); + this.eventEmitter.emit('hook:conversation:end', convo); throw err; } } @@ -405,7 +405,7 @@ export class BotService { return this.triggerBlock(event, updatedConversation, block, false); } catch (err) { this.logger.error('Unable to store context data!', err); - this.eventEmitter.emit('hook:conversation:end', convo, true); + this.eventEmitter.emit('hook:conversation:end', convo); } } catch (err) { this.logger.error('Unable to start a new conversation with ', err); From d705a3efc84154278287c060ca4bb345c5eebf28 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 08:13:20 +0100 Subject: [PATCH 06/58] feat: add cleanup module --- api/src/app.module.ts | 21 ++++++++- api/src/cleanup/cleanup.module.ts | 18 ++++++++ api/src/cleanup/cleanup.service.ts | 62 ++++++++++++++++++++++++++ api/src/cleanup/types.ts | 41 +++++++++++++++++ api/src/utils/test/fixtures/setting.ts | 36 +++++++++++++++ 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 api/src/cleanup/cleanup.module.ts create mode 100644 api/src/cleanup/cleanup.service.ts create mode 100644 api/src/cleanup/types.ts diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 86c35c44..67e296d7 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -11,7 +11,7 @@ import path from 'path'; import { CacheModule } from '@nestjs/cache-manager'; // eslint-disable-next-line import/order import { MailerModule } from '@nestjs-modules/mailer'; -import { Module } from '@nestjs/common'; +import { Module, OnApplicationBootstrap } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { MongooseModule } from '@nestjs/mongoose'; @@ -33,12 +33,15 @@ import { AppService } from './app.service'; import { AttachmentModule } from './attachment/attachment.module'; import { ChannelModule } from './channel/channel.module'; import { ChatModule } from './chat/chat.module'; +import { CleanupModule } from './cleanup/cleanup.module'; +import { CleanupService } from './cleanup/cleanup.service'; import { CmsModule } from './cms/cms.module'; import { config } from './config'; import extraModules from './extra'; import { HelperModule } from './helper/helper.module'; import { I18nModule } from './i18n/i18n.module'; import { LoggerModule } from './logger/logger.module'; +import { LoggerService } from './logger/logger.service'; import { MigrationModule } from './migration/migration.module'; import { NlpModule } from './nlp/nlp.module'; import { PluginsModule } from './plugins/plugins.module'; @@ -152,6 +155,7 @@ const i18nOptions: I18nOptions = { max: config.cache.max, }), MigrationModule, + CleanupModule, ...extraModules, ], controllers: [AppController], @@ -161,4 +165,17 @@ const i18nOptions: I18nOptions = { AppService, ], }) -export class HexabotModule {} +export class HexabotModule implements OnApplicationBootstrap { + constructor( + private readonly loggerService: LoggerService, + private readonly cleanupService: CleanupService, + ) {} + + async onApplicationBootstrap() { + try { + await this.cleanupService.deleteUnusedSettings(); + } catch (error) { + this.loggerService.error('Unable to delete unused settings', error); + } + } +} diff --git a/api/src/cleanup/cleanup.module.ts b/api/src/cleanup/cleanup.module.ts new file mode 100644 index 00000000..e758fc26 --- /dev/null +++ b/api/src/cleanup/cleanup.module.ts @@ -0,0 +1,18 @@ +/* + * 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 { Global, Module } from '@nestjs/common'; + +import { CleanupService } from './cleanup.service'; + +@Global() +@Module({ + providers: [CleanupService], + exports: [CleanupService], +}) +export class CleanupModule {} diff --git a/api/src/cleanup/cleanup.service.ts b/api/src/cleanup/cleanup.service.ts new file mode 100644 index 00000000..c3a241a2 --- /dev/null +++ b/api/src/cleanup/cleanup.service.ts @@ -0,0 +1,62 @@ +/* + * 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 { Injectable } from '@nestjs/common'; + +import { ChannelService } from '@/channel/channel.service'; +import { HelperService } from '@/helper/helper.service'; +import { LoggerService } from '@/logger/logger.service'; +import { SettingService } from '@/setting/services/setting.service'; +import { DeleteResult } from '@/utils/generics/base-repository'; + +import { TCriteria, TExtractExtension, TExtractNamespace } from './types'; + +@Injectable() +export class CleanupService { + constructor( + private readonly helperService: HelperService, + private readonly loggerService: LoggerService, + private readonly settingService: SettingService, + private readonly channelService: ChannelService, + ) {} + + private async deleteMany(criteria: TCriteria[]): Promise { + return await this.settingService.deleteMany({ + $or: criteria.map(({ suffix, namespaces }) => ({ + group: { $regex: suffix, $nin: namespaces }, + })), + }); + } + + public getChannelNamespaces(): TExtractNamespace<'channel'>[] { + return this.channelService + .getAll() + .map((channel) => channel.getNamespace>()); + } + + public getHelperNamespaces(): TExtractNamespace<'helper'>[] { + return this.helperService + .getAll() + .map((helper) => helper.getNamespace>()); + } + + public async deleteUnusedSettings() { + const channels = this.getChannelNamespaces(); + const helpers = this.getHelperNamespaces(); + const { deletedCount } = await this.deleteMany([ + { suffix: '_channel', namespaces: channels }, + { suffix: '_helper', namespaces: helpers }, + ]); + + if (deletedCount > 0) { + this.loggerService.log( + `${deletedCount} unused setting${deletedCount === 1 ? '' : 's'} are successfully deleted!`, + ); + } + } +} diff --git a/api/src/cleanup/types.ts b/api/src/cleanup/types.ts new file mode 100644 index 00000000..2514c1c8 --- /dev/null +++ b/api/src/cleanup/types.ts @@ -0,0 +1,41 @@ +/* + * 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 { ExtensionName } from '@/utils/types/extension'; + +type TExcludedExtension = 'plugin'; + +type TExcludeSuffix< + T, + S extends string = '_', + Suffix extends string = `${S}${TExcludedExtension}`, +> = T extends `${infer _Base}${Suffix}` ? never : T; + +export type TExtensionName = TExcludeSuffix; + +export type TExtension = + Extract extends `${string}-${infer S}` + ? `${S}` + : never; + +export type TNamespace = HyphenToUnderscore; + +export type TExtractNamespace< + T extends TExtension = TExtension, + M extends TExtensionName = TExtensionName, +> = M extends `${string}${T}` ? HyphenToUnderscore : never; + +export type TExtractExtension< + T extends TExtension = TExtension, + M extends TExtensionName = TExtensionName, +> = M extends `${string}${T}` ? M : never; + +export type TCriteria = { + suffix: `_${TExtension}`; + namespaces: TNamespace[]; +}; diff --git a/api/src/utils/test/fixtures/setting.ts b/api/src/utils/test/fixtures/setting.ts index 39ccfadb..7e848aee 100644 --- a/api/src/utils/test/fixtures/setting.ts +++ b/api/src/utils/test/fixtures/setting.ts @@ -11,6 +11,7 @@ import mongoose from 'mongoose'; import { SettingCreateDto } from '@/setting/dto/setting.dto'; import { SettingModel } from '@/setting/schemas/setting.schema'; import { SettingType } from '@/setting/schemas/types'; +import { getRandom } from '@/utils/helpers/safeRandom'; export const settingFixtures: SettingCreateDto[] = [ { @@ -90,6 +91,41 @@ export const settingFixtures: SettingCreateDto[] = [ type: SettingType.text, weight: 10, }, + { + group: `${getRandom()}_channel`, + label: `${getRandom()}`, + value: '', + type: SettingType.text, + weight: 11, + }, + { + group: `${getRandom()}_helper`, + label: `${getRandom()}`, + value: '', + type: SettingType.text, + weight: 12, + }, + { + group: `${getRandom()}_channel`, + label: `${getRandom()}`, + value: '', + type: SettingType.text, + weight: 13, + }, + { + group: `${getRandom()}_helper`, + label: `${getRandom()}`, + value: '', + type: SettingType.text, + weight: 14, + }, + { + group: 'local_storage_helper', + label: 'default storage helper label', + value: 'local-storage-helper', + type: SettingType.text, + weight: 15, + }, ]; export const installSettingFixtures = async () => { From e134ba65e915fe8621a39333c68f0fa04a0a25b5 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 08:16:18 +0100 Subject: [PATCH 07/58] feat: add cleanup.service unit tests --- api/src/cleanup/cleanup.service.spec.ts | 110 ++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 api/src/cleanup/cleanup.service.spec.ts diff --git a/api/src/cleanup/cleanup.service.spec.ts b/api/src/cleanup/cleanup.service.spec.ts new file mode 100644 index 00000000..2f2ce248 --- /dev/null +++ b/api/src/cleanup/cleanup.service.spec.ts @@ -0,0 +1,110 @@ +/* + * 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 { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { MongooseModule } from '@nestjs/mongoose'; + +import { AttachmentRepository } from '@/attachment/repositories/attachment.repository'; +import { AttachmentModel } from '@/attachment/schemas/attachment.schema'; +import { AttachmentService } from '@/attachment/services/attachment.service'; +import { ChannelService } from '@/channel/channel.service'; +import { SubscriberRepository } from '@/chat/repositories/subscriber.repository'; +import { SubscriberModel } from '@/chat/schemas/subscriber.schema'; +import { SubscriberService } from '@/chat/services/subscriber.service'; +import LocalStorageHelper from '@/extensions/helpers/local-storage/index.helper'; +import { HelperService } from '@/helper/helper.service'; +import { LoggerService } from '@/logger/logger.service'; +import { SettingRepository } from '@/setting/repositories/setting.repository'; +import { Setting, SettingModel } from '@/setting/schemas/setting.schema'; +import { SettingSeeder } from '@/setting/seeds/setting.seed'; +import { SettingService } from '@/setting/services/setting.service'; +import { installSettingFixtures } from '@/utils/test/fixtures/setting'; +import { + closeInMongodConnection, + rootMongooseTestModule, +} from '@/utils/test/test'; +import { buildTestingMocks } from '@/utils/test/utils'; + +import { CleanupService } from './cleanup.service'; +import { TNamespace } from './types'; + +describe('CleanupService', () => { + let initialSettings: Setting[]; + let helperService: HelperService; + let cleanupService: CleanupService; + let settingService: SettingService; + + beforeAll(async () => { + const { getMocks, resolveMocks } = await buildTestingMocks({ + imports: [ + rootMongooseTestModule(installSettingFixtures), + MongooseModule.forFeature([ + SettingModel, + SubscriberModel, + AttachmentModel, + ]), + ], + providers: [ + CleanupService, + HelperService, + SettingService, + SettingRepository, + { + provide: CACHE_MANAGER, + useValue: { + del: jest.fn(), + get: jest.fn(), + set: jest.fn(), + }, + }, + SettingSeeder, + SubscriberService, + SubscriberRepository, + AttachmentService, + AttachmentRepository, + ChannelService, + ], + }); + [cleanupService, settingService, helperService] = await getMocks([ + CleanupService, + SettingService, + HelperService, + , + ]); + + const [loggerService] = await resolveMocks([LoggerService]); + initialSettings = await settingService.findAll(); + + helperService.register( + new LocalStorageHelper(settingService, helperService, loggerService), + ); + }); + + afterAll(closeInMongodConnection); + + afterEach(jest.clearAllMocks); + + describe('delete', () => { + it('should delete all the unregistered settings with a group suffix `_channel` or/and `_helper`', async () => { + const registeredNamespaces = [ + ...cleanupService.getChannelNamespaces(), + ...cleanupService.getHelperNamespaces(), + ]; + + await cleanupService.deleteUnusedSettings(); + const cleanSettings = await settingService.findAll(); + const filteredSettings = initialSettings.filter( + ({ group }) => + !/_(channel|helper)$/.test(group) !== + registeredNamespaces.includes(group as TNamespace), + ); + + expect(cleanSettings).toEqualPayload(filteredSettings); + }); + }); +}); From cf350c1048909453809a56f98a73c05041897c43 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 08:52:32 +0100 Subject: [PATCH 08/58] fix: apply feedback updates --- api/src/app.module.ts | 23 ++++--------------- .../cleanup.service.spec.ts | 2 +- .../{cleanup => extention}/cleanup.service.ts | 10 ++++---- .../extension.module.ts} | 19 +++++++++++++-- api/src/{cleanup => extention}/types.ts | 0 5 files changed, 28 insertions(+), 26 deletions(-) rename api/src/{cleanup => extention}/cleanup.service.spec.ts (98%) rename api/src/{cleanup => extention}/cleanup.service.ts (88%) rename api/src/{cleanup/cleanup.module.ts => extention/extension.module.ts} (55%) rename api/src/{cleanup => extention}/types.ts (100%) diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 67e296d7..941ec354 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -11,7 +11,7 @@ import path from 'path'; import { CacheModule } from '@nestjs/cache-manager'; // eslint-disable-next-line import/order import { MailerModule } from '@nestjs-modules/mailer'; -import { Module, OnApplicationBootstrap } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { MongooseModule } from '@nestjs/mongoose'; @@ -33,15 +33,13 @@ import { AppService } from './app.service'; import { AttachmentModule } from './attachment/attachment.module'; import { ChannelModule } from './channel/channel.module'; import { ChatModule } from './chat/chat.module'; -import { CleanupModule } from './cleanup/cleanup.module'; -import { CleanupService } from './cleanup/cleanup.service'; import { CmsModule } from './cms/cms.module'; import { config } from './config'; +import { ExtensionModule } from './extention/extension.module'; import extraModules from './extra'; import { HelperModule } from './helper/helper.module'; import { I18nModule } from './i18n/i18n.module'; import { LoggerModule } from './logger/logger.module'; -import { LoggerService } from './logger/logger.service'; import { MigrationModule } from './migration/migration.module'; import { NlpModule } from './nlp/nlp.module'; import { PluginsModule } from './plugins/plugins.module'; @@ -155,7 +153,7 @@ const i18nOptions: I18nOptions = { max: config.cache.max, }), MigrationModule, - CleanupModule, + ExtensionModule, ...extraModules, ], controllers: [AppController], @@ -165,17 +163,4 @@ const i18nOptions: I18nOptions = { AppService, ], }) -export class HexabotModule implements OnApplicationBootstrap { - constructor( - private readonly loggerService: LoggerService, - private readonly cleanupService: CleanupService, - ) {} - - async onApplicationBootstrap() { - try { - await this.cleanupService.deleteUnusedSettings(); - } catch (error) { - this.loggerService.error('Unable to delete unused settings', error); - } - } -} +export class HexabotModule {} diff --git a/api/src/cleanup/cleanup.service.spec.ts b/api/src/extention/cleanup.service.spec.ts similarity index 98% rename from api/src/cleanup/cleanup.service.spec.ts rename to api/src/extention/cleanup.service.spec.ts index 2f2ce248..6b587ae0 100644 --- a/api/src/cleanup/cleanup.service.spec.ts +++ b/api/src/extention/cleanup.service.spec.ts @@ -96,7 +96,7 @@ describe('CleanupService', () => { ...cleanupService.getHelperNamespaces(), ]; - await cleanupService.deleteUnusedSettings(); + await cleanupService.pruneExtensionSettings(); const cleanSettings = await settingService.findAll(); const filteredSettings = initialSettings.filter( ({ group }) => diff --git a/api/src/cleanup/cleanup.service.ts b/api/src/extention/cleanup.service.ts similarity index 88% rename from api/src/cleanup/cleanup.service.ts rename to api/src/extention/cleanup.service.ts index c3a241a2..994ed5a3 100644 --- a/api/src/cleanup/cleanup.service.ts +++ b/api/src/extention/cleanup.service.ts @@ -25,7 +25,9 @@ export class CleanupService { private readonly channelService: ChannelService, ) {} - private async deleteMany(criteria: TCriteria[]): Promise { + private async deleteManyBySuffixAndNamespaces( + criteria: TCriteria[], + ): Promise { return await this.settingService.deleteMany({ $or: criteria.map(({ suffix, namespaces }) => ({ group: { $regex: suffix, $nin: namespaces }, @@ -45,17 +47,17 @@ export class CleanupService { .map((helper) => helper.getNamespace>()); } - public async deleteUnusedSettings() { + public async pruneExtensionSettings() { const channels = this.getChannelNamespaces(); const helpers = this.getHelperNamespaces(); - const { deletedCount } = await this.deleteMany([ + const { deletedCount } = await this.deleteManyBySuffixAndNamespaces([ { suffix: '_channel', namespaces: channels }, { suffix: '_helper', namespaces: helpers }, ]); if (deletedCount > 0) { this.loggerService.log( - `${deletedCount} unused setting${deletedCount === 1 ? '' : 's'} are successfully deleted!`, + `${deletedCount} unused setting${deletedCount === 1 ? '' : 's'} ${deletedCount === 1 ? 'is' : 'are'} successfully deleted!`, ); } } diff --git a/api/src/cleanup/cleanup.module.ts b/api/src/extention/extension.module.ts similarity index 55% rename from api/src/cleanup/cleanup.module.ts rename to api/src/extention/extension.module.ts index e758fc26..8d43c8ab 100644 --- a/api/src/cleanup/cleanup.module.ts +++ b/api/src/extention/extension.module.ts @@ -6,7 +6,9 @@ * 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 { Global, Module } from '@nestjs/common'; +import { Global, Module, OnApplicationBootstrap } from '@nestjs/common'; + +import { LoggerService } from '@/logger/logger.service'; import { CleanupService } from './cleanup.service'; @@ -15,4 +17,17 @@ import { CleanupService } from './cleanup.service'; providers: [CleanupService], exports: [CleanupService], }) -export class CleanupModule {} +export class ExtensionModule implements OnApplicationBootstrap { + constructor( + private readonly loggerService: LoggerService, + private readonly cleanupService: CleanupService, + ) {} + + async onApplicationBootstrap() { + try { + await this.cleanupService.pruneExtensionSettings(); + } catch (error) { + this.loggerService.error('Unable to delete unused settings', error); + } + } +} diff --git a/api/src/cleanup/types.ts b/api/src/extention/types.ts similarity index 100% rename from api/src/cleanup/types.ts rename to api/src/extention/types.ts From d73e4dc8cbb0bdf8444243321156126e3b1d1df5 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 08:55:41 +0100 Subject: [PATCH 09/58] fix: rename the module --- api/src/app.module.ts | 2 +- api/src/{extention => extension}/cleanup.service.spec.ts | 0 api/src/{extention => extension}/cleanup.service.ts | 0 api/src/{extention => extension}/extension.module.ts | 0 api/src/{extention => extension}/types.ts | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename api/src/{extention => extension}/cleanup.service.spec.ts (100%) rename api/src/{extention => extension}/cleanup.service.ts (100%) rename api/src/{extention => extension}/extension.module.ts (100%) rename api/src/{extention => extension}/types.ts (100%) diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 941ec354..72465eb3 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -35,7 +35,7 @@ import { ChannelModule } from './channel/channel.module'; import { ChatModule } from './chat/chat.module'; import { CmsModule } from './cms/cms.module'; import { config } from './config'; -import { ExtensionModule } from './extention/extension.module'; +import { ExtensionModule } from './extension/extension.module'; import extraModules from './extra'; import { HelperModule } from './helper/helper.module'; import { I18nModule } from './i18n/i18n.module'; diff --git a/api/src/extention/cleanup.service.spec.ts b/api/src/extension/cleanup.service.spec.ts similarity index 100% rename from api/src/extention/cleanup.service.spec.ts rename to api/src/extension/cleanup.service.spec.ts diff --git a/api/src/extention/cleanup.service.ts b/api/src/extension/cleanup.service.ts similarity index 100% rename from api/src/extention/cleanup.service.ts rename to api/src/extension/cleanup.service.ts diff --git a/api/src/extention/extension.module.ts b/api/src/extension/extension.module.ts similarity index 100% rename from api/src/extention/extension.module.ts rename to api/src/extension/extension.module.ts diff --git a/api/src/extention/types.ts b/api/src/extension/types.ts similarity index 100% rename from api/src/extention/types.ts rename to api/src/extension/types.ts From 73681948d361e6dc95ba99b3ea85c6a0b73d6f73 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Fri, 4 Apr 2025 11:01:57 +0100 Subject: [PATCH 10/58] fix: keep query params state when navigating to different conversation --- .../components/inbox/components/ConversationsList.tsx | 6 +++++- frontend/src/utils/URL.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/inbox/components/ConversationsList.tsx b/frontend/src/components/inbox/components/ConversationsList.tsx index 66409f85..f9ac6704 100644 --- a/frontend/src/components/inbox/components/ConversationsList.tsx +++ b/frontend/src/components/inbox/components/ConversationsList.tsx @@ -20,6 +20,7 @@ import { useConfig } from "@/hooks/useConfig"; import { useTranslate } from "@/hooks/useTranslate"; import { Title } from "@/layout/content/Title"; import { EntityType, RouterType } from "@/services/types"; +import { extractQueryParamsUrl } from "@/utils/URL"; import { getAvatarSrc } from "../helpers/mapMessages"; import { useChat } from "../hooks/ChatContext"; @@ -64,7 +65,10 @@ export const SubscribersList = (props: { { chat.setSubscriberId(subscriber.id); - push(`/${RouterType.INBOX}/subscribers/${subscriber.id}`); + push({ + pathname: `/${RouterType.INBOX}/subscribers/${subscriber.id}`, + query: extractQueryParamsUrl(window.location.href), + }); }} className="changeColor" key={subscriber.id} diff --git a/frontend/src/utils/URL.ts b/frontend/src/utils/URL.ts index 0c85749d..e169dbe2 100644 --- a/frontend/src/utils/URL.ts +++ b/frontend/src/utils/URL.ts @@ -6,6 +6,8 @@ * 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 getFromQuery = ({ key, search, @@ -57,3 +59,12 @@ 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 2495edf21d0ab0c826176c3ef32442a6072170d3 Mon Sep 17 00:00:00 2001 From: hexastack Date: Mon, 27 Jan 2025 18:26:24 +0100 Subject: [PATCH 11/58] fix: hook stats entry event --- .../analytics/services/bot-stats.service.ts | 9 ++++- .../repositories/subscriber.repository.ts | 3 +- api/src/chat/services/bot.service.spec.ts | 6 +-- api/src/chat/services/bot.service.ts | 37 +++++++++++++++---- api/src/chat/services/chat.service.ts | 13 +++++-- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/api/src/analytics/services/bot-stats.service.ts b/api/src/analytics/services/bot-stats.service.ts index 2e22a519..4d87f64b 100644 --- a/api/src/analytics/services/bot-stats.service.ts +++ b/api/src/analytics/services/bot-stats.service.ts @@ -93,8 +93,9 @@ export class BotStatsService extends BaseService { ) { this.eventEmitter.emit( 'hook:stats:entry', - 'retention', + BotStatsType.retention, 'Retentioned users', + subscriber, ); } } @@ -106,7 +107,11 @@ export class BotStatsService extends BaseService { * @param name - The name or identifier of the statistics entry (e.g., a specific feature or component being tracked). */ @OnEvent('hook:stats:entry') - async handleStatEntry(type: BotStatsType, name: string): Promise { + async handleStatEntry( + type: BotStatsType, + name: string, + _subscriber: Subscriber, + ): Promise { const day = new Date(); day.setMilliseconds(0); day.setSeconds(0); diff --git a/api/src/chat/repositories/subscriber.repository.ts b/api/src/chat/repositories/subscriber.repository.ts index 10974ba4..f6eb4d57 100644 --- a/api/src/chat/repositories/subscriber.repository.ts +++ b/api/src/chat/repositories/subscriber.repository.ts @@ -16,6 +16,7 @@ import { UpdateWithAggregationPipeline, } from 'mongoose'; +import { BotStatsType } from '@/analytics/schemas/bot-stats.schema'; import { BaseRepository } from '@/utils/generics/base-repository'; import { TFilterQuery } from '@/utils/types/filter.types'; @@ -47,7 +48,7 @@ export class SubscriberRepository extends BaseRepository< async postCreate(created: SubscriberDocument): Promise { this.eventEmitter.emit( 'hook:stats:entry', - 'new_users', + BotStatsType.new_users, 'New users', created, ); diff --git a/api/src/chat/services/bot.service.spec.ts b/api/src/chat/services/bot.service.spec.ts index 794f6420..820cff08 100644 --- a/api/src/chat/services/bot.service.spec.ts +++ b/api/src/chat/services/bot.service.spec.ts @@ -243,8 +243,8 @@ describe('BlockService', () => { await botService.startConversation(event, block); expect(hasBotSpoken).toEqual(true); expect(triggeredEvents).toEqual([ - ['popular', 'hasNextBlocks'], - ['new_conversations', 'New conversations'], + ['popular', 'hasNextBlocks', webSubscriber], + ['new_conversations', 'New conversations', webSubscriber], ]); clearMock.mockClear(); }); @@ -301,7 +301,7 @@ describe('BlockService', () => { const captured = await botService.processConversationMessage(event); expect(captured).toBe(true); expect(triggeredEvents).toEqual([ - ['existing_conversations', 'Existing conversations'], + ['existing_conversations', 'Existing conversations', webSubscriber], ]); clearMock.mockClear(); }); diff --git a/api/src/chat/services/bot.service.ts b/api/src/chat/services/bot.service.ts index 46341b54..ef29142e 100644 --- a/api/src/chat/services/bot.service.ts +++ b/api/src/chat/services/bot.service.ts @@ -9,6 +9,7 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { BotStatsType } from '@/analytics/schemas/bot-stats.schema'; import EventWrapper from '@/channel/lib/EventWrapper'; import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; @@ -65,8 +66,18 @@ export class BotService { .getHandler() .sendMessage(event, envelope, options, context); - this.eventEmitter.emit('hook:stats:entry', 'outgoing', 'Outgoing'); - this.eventEmitter.emit('hook:stats:entry', 'all_messages', 'All Messages'); + this.eventEmitter.emit( + 'hook:stats:entry', + BotStatsType.outgoing, + 'Outgoing', + recipient, + ); + this.eventEmitter.emit( + 'hook:stats:entry', + BotStatsType.all_messages, + 'All Messages', + recipient, + ); // Trigger sent message event const sentMessage: MessageCreateDto = { @@ -293,7 +304,12 @@ export class BotService { if (next) { // Increment stats about popular blocks - this.eventEmitter.emit('hook:stats:entry', 'popular', next.name); + this.eventEmitter.emit( + 'hook:stats:entry', + BotStatsType.popular, + next.name, + convo.sender, + ); // Go next! this.logger.debug('Respond to nested conversion! Go next ', next.id); try { @@ -352,8 +368,9 @@ export class BotService { this.eventEmitter.emit( 'hook:stats:entry', - 'existing_conversations', + BotStatsType.existing_conversations, 'Existing conversations', + subscriber, ); this.logger.debug('Conversation has been captured! Responding ...'); return await this.handleIncomingMessage(conversation, event); @@ -373,10 +390,15 @@ export class BotService { * @param block - Starting block */ async startConversation(event: EventWrapper, block: BlockFull) { - // Increment popular stats - this.eventEmitter.emit('hook:stats:entry', 'popular', block.name); // Launching a new conversation const subscriber = event.getSender(); + // Increment popular stats + this.eventEmitter.emit( + 'hook:stats:entry', + BotStatsType.popular, + block.name, + subscriber, + ); try { const convo = await this.conversationService.create({ @@ -384,8 +406,9 @@ export class BotService { }); this.eventEmitter.emit( 'hook:stats:entry', - 'new_conversations', + BotStatsType.new_conversations, 'New conversations', + subscriber, ); try { diff --git a/api/src/chat/services/chat.service.ts b/api/src/chat/services/chat.service.ts index 36e75db6..eec6fb84 100644 --- a/api/src/chat/services/chat.service.ts +++ b/api/src/chat/services/chat.service.ts @@ -11,6 +11,7 @@ import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import mime from 'mime'; import { v4 as uuidv4 } from 'uuid'; +import { BotStatsType } from '@/analytics/schemas/bot-stats.schema'; import { AttachmentService } from '@/attachment/services/attachment.service'; import { AttachmentAccess, @@ -149,11 +150,17 @@ export class ChatService { } this.websocketGateway.broadcastMessageReceived(populatedMsg, subscriber); - this.eventEmitter.emit('hook:stats:entry', 'incoming', 'Incoming'); this.eventEmitter.emit( 'hook:stats:entry', - 'all_messages', + BotStatsType.incoming, + 'Incoming', + subscriber, + ); + this.eventEmitter.emit( + 'hook:stats:entry', + BotStatsType.all_messages, 'All Messages', + subscriber, ); } catch (err) { this.logger.error('Unable to log received message.', err, event); @@ -248,7 +255,7 @@ export class ChatService { }; this.eventEmitter.emit('hook:chatbot:sent', sentMessage, event); - this.eventEmitter.emit('hook:stats:entry', 'echo', 'Echo'); + this.eventEmitter.emit('hook:stats:entry', 'echo', 'Echo', recipient); } catch (err) { this.logger.error('Unable to log echo message', err, event); } From 100cab6c512f76078c6d8ce7d89b7868ad6a82fa Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Fri, 4 Apr 2025 11:43:58 +0100 Subject: [PATCH 12/58] fix: editText, addText warnning attribute not recognized by html --- frontend/src/app-components/dialogs/GenericFormDialog.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app-components/dialogs/GenericFormDialog.tsx b/frontend/src/app-components/dialogs/GenericFormDialog.tsx index f73e0e57..df387ecb 100644 --- a/frontend/src/app-components/dialogs/GenericFormDialog.tsx +++ b/frontend/src/app-components/dialogs/GenericFormDialog.tsx @@ -24,11 +24,13 @@ export const GenericFormDialog = ({ Form, rowKey, payload: data, + editText, + addText, ...rest }: GenericFormDialogProps) => { const { t } = useTranslate(); const hasRow = rowKey ? data?.[rowKey] : data; - const translationKey = hasRow ? rest.editText : rest.addText; + const translationKey = hasRow ? editText : addText; return (
Date: Fri, 21 Mar 2025 14:52:45 +0100 Subject: [PATCH 13/58] feat: support the display samples count per value --- .../nlp/controllers/nlp-value.controller.ts | 18 ++- api/src/nlp/services/nlp-value.service.ts | 124 +++++++++++++++++- frontend/public/locales/en/translation.json | 1 + frontend/public/locales/fr/translation.json | 1 + .../components/nlp/components/NlpValue.tsx | 19 ++- frontend/src/types/nlp-value.types.ts | 1 + 6 files changed, 160 insertions(+), 4 deletions(-) diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index a8c3c6be..42cb334f 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -125,6 +125,22 @@ export class NlpValueController extends BaseController< return doc; } + @Get('') + async findAndPopulateNlpValuesWithCount( + @Query(PageQueryPipe) pageQuery: PageQueryDto, + @Query(PopulatePipe) populate: string[], + @Query( + new SearchFilterPipe({ allowedFields: ['entity', 'value'] }), + ) + filters?: TFilterQuery, + ) { + return await this.nlpValueService.findAndPopulateNlpValuesWithCount( + populate, + filters, + pageQuery, + ); + } + /** * Retrieves a paginated list of NLP values. * @@ -136,7 +152,7 @@ export class NlpValueController extends BaseController< * * @returns A promise resolving to a paginated list of NLP values. */ - @Get() + // @Get('') disabled async findPage( @Query(PageQueryPipe) pageQuery: PageQueryDto, @Query(PopulatePipe) populate: string[], diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index f246ad61..c9ec6309 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -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. @@ -7,11 +7,15 @@ */ import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Types } from 'mongoose'; import { DeleteResult } from '@/utils/generics/base-repository'; import { BaseService } from '@/utils/generics/base-service'; +import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; +import { TFilterQuery } from '@/utils/types/filter.types'; import { NlpValueCreateDto, NlpValueDto } from '../dto/nlp-value.dto'; +import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository'; import { NlpValueRepository } from '../repositories/nlp-value.repository'; import { NlpEntity } from '../schemas/nlp-entity.schema'; import { @@ -34,6 +38,7 @@ export class NlpValueService extends BaseService< readonly repository: NlpValueRepository, @Inject(forwardRef(() => NlpEntityService)) private readonly nlpEntityService: NlpEntityService, + private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, ) { super(repository); } @@ -218,4 +223,121 @@ export class NlpValueService extends BaseService< }); return Promise.all(promises); } + + async findAndPopulateNlpValuesWithCount( + populate: string[], + filters?: TFilterQuery, + pageQuery?: PageQueryDto, + ) { + const { $and = [], ...rest } = filters || ({} as TFilterQuery); + + const entityValueModel = this.getRepository().model; + + return entityValueModel + .aggregate([ + { + // support filters + $match: { + ...rest, + ...($and?.length && { + $and: + $and?.map((v) => { + if (v.entity) { + return { + ...v, + entity: new Types.ObjectId(String(v.entity)), + }; + } + + return v; + }) || [], + }), + }, + }, + // support pageQuery + { + $skip: pageQuery?.skip || 0, + }, + { + $limit: pageQuery?.limit || 10, + }, + { + $sort: { + [pageQuery?.sort?.[0] || 'createdAt']: + pageQuery?.sort?.[1] === 'desc' ? -1 : 1, + }, + }, + { + $lookup: { + from: 'nlpsampleentities', + localField: '_id', + foreignField: 'value', + as: 'sampleEntities', + }, + }, + { + $unwind: { + path: '$sampleEntities', + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: 'nlpsamples', + localField: 'sampleEntities.sample', + foreignField: '_id', + as: 'samples', + }, + }, + { + $lookup: { + from: 'nlpentities', + localField: 'entity', + foreignField: '_id', + as: 'entities', + }, + }, + { + $group: { + _id: '$_id', + value: { $first: '$value' }, + expressions: { $first: '$expressions' }, + builtin: { $first: '$builtin' }, + metadata: { $first: '$metadata' }, + createdAt: { $first: '$createdAt' }, + updatedAt: { $first: '$updatedAt' }, + entity: { + // support populate + $first: populate.some((p) => + this.getRepository() + .getPopulate() + .map((p) => p.toString()) + .includes(p), + ) + ? '$entities' + : '$entity', + }, + //TODO when samples is empty array we need to return 0 not 1 + nlpSamplesCount: { + $sum: { $cond: [{ $ifNull: ['samples', false] }, 1, 0] }, + }, + }, + }, + { + $project: { + id: '$_id', + _id: 0, + value: 1, + expressions: 1, + builtin: 1, + entity: 1, + metadata: 1, + createdAt: 1, + updatedAt: 1, + nlpSamplesCount: 1, + }, + }, + ]) + .exec(); + } } diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 93781e15..b243c3ae 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -334,6 +334,7 @@ "nlp": "NLU", "nlp_entity": "Entity", "nlp_entity_value": "Value", + "nlp_samples_count": "Nlp Samples count", "value": "Value", "synonyms": "Synonyms", "lookups": "Lookups", diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index 208dfc25..cd32248c 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -334,6 +334,7 @@ "nlp": "NLU", "nlp_entity": "Entité NLU", "nlp_entity_value": "Valeur NLU", + "nlp_samples_count": "Nombre des échantillons", "value": "Valeur", "lookups": "Stratégies", "lookup_strategies": "Stratégie de recherche", diff --git a/frontend/src/components/nlp/components/NlpValue.tsx b/frontend/src/components/nlp/components/NlpValue.tsx index 12c462e8..92cc7d04 100644 --- a/frontend/src/components/nlp/components/NlpValue.tsx +++ b/frontend/src/components/nlp/components/NlpValue.tsx @@ -55,7 +55,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { const canHaveSynonyms = nlpEntity?.lookups?.[0] === NlpLookups.keywords; const { onSearch, searchPayload } = useSearch({ $eq: [{ entity: entityId }], - $or: ["doc", "value"] + $or: ["doc", "value"], }); const { dataGridProps } = useFind( { entity: EntityType.NLP_VALUE }, @@ -103,7 +103,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { ], t("label.operations"), ); - const synonymsColumn = { + const synonymsColumn = { flex: 3, field: "synonyms", headerName: t("label.synonyms"), @@ -125,6 +125,21 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { disableColumnMenu: true, renderHeader, }, + { + flex: 3, + field: "nlpSamplesCount", + headerName: t("label.nlp_samples_count"), + sortable: true, + disableColumnMenu: true, + renderHeader, + renderCell: ({ row }) => ( + + ), + }, { flex: 3, field: "doc", diff --git a/frontend/src/types/nlp-value.types.ts b/frontend/src/types/nlp-value.types.ts index 7b7a1e5e..4986ee8d 100644 --- a/frontend/src/types/nlp-value.types.ts +++ b/frontend/src/types/nlp-value.types.ts @@ -19,6 +19,7 @@ export interface INlpValueAttributes { expressions?: string[]; metadata?: Record; builtin?: boolean; + nlpSamplesCount?: number; } export interface INlpValueStub extends IBaseSchema, INlpValueAttributes {} From 7ecf821c48b2acb1b085b5b1f8dbae770ac25fc6 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 21 Mar 2025 15:49:27 +0100 Subject: [PATCH 14/58] fix: move findAndPopulateNlpValuesWithCount to the repository --- .../nlp/repositories/nlp-value.repository.ts | 117 +++++++++++++++++- api/src/nlp/services/nlp-value.service.ts | 116 +---------------- 2 files changed, 121 insertions(+), 112 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index a476c79e..99cfdd11 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -8,9 +8,10 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Document, Model, Query } from 'mongoose'; +import { Document, Model, Query, Types } from 'mongoose'; import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; +import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { TFilterQuery } from '@/utils/types/filter.types'; import { NlpValueDto } from '../dto/nlp-value.dto'; @@ -106,4 +107,118 @@ export class NlpValueRepository extends BaseRepository< throw new Error('Attempted to delete a NLP value using unknown criteria'); } } + + async findAndPopulateNlpValuesWithCount( + populate: string[], + filters?: TFilterQuery, + pageQuery?: PageQueryDto, + ) { + const { $and = [], ...rest } = filters || ({} as TFilterQuery); + + return this.model + .aggregate([ + { + // support filters + $match: { + ...rest, + ...($and?.length && { + $and: + $and?.map((v) => { + if (v.entity) { + return { + ...v, + entity: new Types.ObjectId(String(v.entity)), + }; + } + + return v; + }) || [], + }), + }, + }, + // support pageQuery + { + $skip: pageQuery?.skip || 0, + }, + { + $limit: pageQuery?.limit || 10, + }, + { + $sort: { + [pageQuery?.sort?.[0] || 'createdAt']: + pageQuery?.sort?.[1] === 'desc' ? -1 : 1, + }, + }, + { + $lookup: { + from: 'nlpsampleentities', + localField: '_id', + foreignField: 'value', + as: 'sampleEntities', + }, + }, + { + $unwind: { + path: '$sampleEntities', + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: 'nlpsamples', + localField: 'sampleEntities.sample', + foreignField: '_id', + as: 'samples', + }, + }, + { + $lookup: { + from: 'nlpentities', + localField: 'entity', + foreignField: '_id', + as: 'entities', + }, + }, + { + $group: { + _id: '$_id', + value: { $first: '$value' }, + expressions: { $first: '$expressions' }, + builtin: { $first: '$builtin' }, + metadata: { $first: '$metadata' }, + createdAt: { $first: '$createdAt' }, + updatedAt: { $first: '$updatedAt' }, + entity: { + // support populate + $first: populate.some((p) => + this.getPopulate() + .map((p) => p.toString()) + .includes(p), + ) + ? '$entities' + : '$entity', + }, + //TODO when samples is empty array we need to return 0 not 1 + nlpSamplesCount: { + $sum: { $cond: [{ $ifNull: ['samples', false] }, 1, 0] }, + }, + }, + }, + { + $project: { + id: '$_id', + _id: 0, + value: 1, + expressions: 1, + builtin: 1, + entity: 1, + metadata: 1, + createdAt: 1, + updatedAt: 1, + nlpSamplesCount: 1, + }, + }, + ]) + .exec(); + } } diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index c9ec6309..f4147333 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -7,7 +7,6 @@ */ import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { Types } from 'mongoose'; import { DeleteResult } from '@/utils/generics/base-repository'; import { BaseService } from '@/utils/generics/base-service'; @@ -229,115 +228,10 @@ export class NlpValueService extends BaseService< filters?: TFilterQuery, pageQuery?: PageQueryDto, ) { - const { $and = [], ...rest } = filters || ({} as TFilterQuery); - - const entityValueModel = this.getRepository().model; - - return entityValueModel - .aggregate([ - { - // support filters - $match: { - ...rest, - ...($and?.length && { - $and: - $and?.map((v) => { - if (v.entity) { - return { - ...v, - entity: new Types.ObjectId(String(v.entity)), - }; - } - - return v; - }) || [], - }), - }, - }, - // support pageQuery - { - $skip: pageQuery?.skip || 0, - }, - { - $limit: pageQuery?.limit || 10, - }, - { - $sort: { - [pageQuery?.sort?.[0] || 'createdAt']: - pageQuery?.sort?.[1] === 'desc' ? -1 : 1, - }, - }, - { - $lookup: { - from: 'nlpsampleentities', - localField: '_id', - foreignField: 'value', - as: 'sampleEntities', - }, - }, - { - $unwind: { - path: '$sampleEntities', - preserveNullAndEmptyArrays: true, - }, - }, - { - $lookup: { - from: 'nlpsamples', - localField: 'sampleEntities.sample', - foreignField: '_id', - as: 'samples', - }, - }, - { - $lookup: { - from: 'nlpentities', - localField: 'entity', - foreignField: '_id', - as: 'entities', - }, - }, - { - $group: { - _id: '$_id', - value: { $first: '$value' }, - expressions: { $first: '$expressions' }, - builtin: { $first: '$builtin' }, - metadata: { $first: '$metadata' }, - createdAt: { $first: '$createdAt' }, - updatedAt: { $first: '$updatedAt' }, - entity: { - // support populate - $first: populate.some((p) => - this.getRepository() - .getPopulate() - .map((p) => p.toString()) - .includes(p), - ) - ? '$entities' - : '$entity', - }, - //TODO when samples is empty array we need to return 0 not 1 - nlpSamplesCount: { - $sum: { $cond: [{ $ifNull: ['samples', false] }, 1, 0] }, - }, - }, - }, - { - $project: { - id: '$_id', - _id: 0, - value: 1, - expressions: 1, - builtin: 1, - entity: 1, - metadata: 1, - createdAt: 1, - updatedAt: 1, - nlpSamplesCount: 1, - }, - }, - ]) - .exec(); + return await this.repository.findAndPopulateNlpValuesWithCount( + populate, + filters, + pageQuery, + ); } } From 6a25d47349c251caa447e85c0d1e1ad6ab6574ae Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 23 Mar 2025 13:03:56 +0100 Subject: [PATCH 15/58] fix: enhance logic --- .../nlp/controllers/nlp-value.controller.ts | 4 +- .../nlp/repositories/nlp-value.repository.ts | 60 +++++-------------- api/src/nlp/services/nlp-value.service.ts | 8 +-- 3 files changed, 21 insertions(+), 51 deletions(-) diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index 42cb334f..f1e09dad 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -132,12 +132,12 @@ export class NlpValueController extends BaseController< @Query( new SearchFilterPipe({ allowedFields: ['entity', 'value'] }), ) - filters?: TFilterQuery, + filters: TFilterQuery, ) { return await this.nlpValueService.findAndPopulateNlpValuesWithCount( + pageQuery, populate, filters, - pageQuery, ); } diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 99cfdd11..5c5af5bd 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -109,44 +109,39 @@ export class NlpValueRepository extends BaseRepository< } async findAndPopulateNlpValuesWithCount( + { limit = 10, skip = 0, sort = ['createdAt', -1] }: PageQueryDto, populate: string[], - filters?: TFilterQuery, - pageQuery?: PageQueryDto, + { $and = [], ...rest }: TFilterQuery, ) { - const { $and = [], ...rest } = filters || ({} as TFilterQuery); - return this.model .aggregate([ { // support filters $match: { ...rest, - ...($and?.length && { + ...($and.length && { $and: - $and?.map((v) => { - if (v.entity) { - return { - ...v, - entity: new Types.ObjectId(String(v.entity)), - }; - } - - return v; - }) || [], + $and.map(({ entity, ...rest }) => + entity + ? { + ...rest, + entity: new Types.ObjectId(String(entity)), + } + : rest, + ) || [], }), }, }, // support pageQuery { - $skip: pageQuery?.skip || 0, + $limit: limit, }, { - $limit: pageQuery?.limit || 10, + $skip: skip, }, { $sort: { - [pageQuery?.sort?.[0] || 'createdAt']: - pageQuery?.sort?.[1] === 'desc' ? -1 : 1, + [sort[0]]: sort[1] === 'desc' ? -1 : 1, }, }, { @@ -163,22 +158,6 @@ export class NlpValueRepository extends BaseRepository< preserveNullAndEmptyArrays: true, }, }, - { - $lookup: { - from: 'nlpsamples', - localField: 'sampleEntities.sample', - foreignField: '_id', - as: 'samples', - }, - }, - { - $lookup: { - from: 'nlpentities', - localField: 'entity', - foreignField: '_id', - as: 'entities', - }, - }, { $group: { _id: '$_id', @@ -190,17 +169,10 @@ export class NlpValueRepository extends BaseRepository< updatedAt: { $first: '$updatedAt' }, entity: { // support populate - $first: populate.some((p) => - this.getPopulate() - .map((p) => p.toString()) - .includes(p), - ) - ? '$entities' - : '$entity', + $first: this.canPopulate(populate) ? '$entities' : '$entity', }, - //TODO when samples is empty array we need to return 0 not 1 nlpSamplesCount: { - $sum: { $cond: [{ $ifNull: ['samples', false] }, 1, 0] }, + $sum: { $cond: [{ $ifNull: ['$sampleEntities', false] }, 1, 0] }, }, }, }, diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index f4147333..71ccd031 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -14,7 +14,6 @@ import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { TFilterQuery } from '@/utils/types/filter.types'; import { NlpValueCreateDto, NlpValueDto } from '../dto/nlp-value.dto'; -import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository'; import { NlpValueRepository } from '../repositories/nlp-value.repository'; import { NlpEntity } from '../schemas/nlp-entity.schema'; import { @@ -37,7 +36,6 @@ export class NlpValueService extends BaseService< readonly repository: NlpValueRepository, @Inject(forwardRef(() => NlpEntityService)) private readonly nlpEntityService: NlpEntityService, - private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, ) { super(repository); } @@ -224,14 +222,14 @@ export class NlpValueService extends BaseService< } async findAndPopulateNlpValuesWithCount( + pageQuery: PageQueryDto, populate: string[], - filters?: TFilterQuery, - pageQuery?: PageQueryDto, + filters: TFilterQuery, ) { return await this.repository.findAndPopulateNlpValuesWithCount( + pageQuery, populate, filters, - pageQuery, ); } } From 61d8938212cd8e4dae2f3578ae1d67bd066b925d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 23 Mar 2025 15:05:51 +0100 Subject: [PATCH 16/58] fix: add nlpentities populate --- api/src/nlp/repositories/nlp-value.repository.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 5c5af5bd..8d68a654 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -158,6 +158,14 @@ export class NlpValueRepository extends BaseRepository< preserveNullAndEmptyArrays: true, }, }, + { + $lookup: { + from: 'nlpentities', + localField: 'entity', + foreignField: '_id', + as: 'entities', + }, + }, { $group: { _id: '$_id', From 486def7e7566eb83124b820d320f5575beca892d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 23 Mar 2025 15:14:13 +0100 Subject: [PATCH 17/58] fix: rename findAndPopulate method --- api/src/nlp/controllers/nlp-value.controller.ts | 4 ++-- api/src/nlp/repositories/nlp-value.repository.ts | 4 ++-- api/src/nlp/services/nlp-value.service.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index f1e09dad..6c8c0e42 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -126,7 +126,7 @@ export class NlpValueController extends BaseController< } @Get('') - async findAndPopulateNlpValuesWithCount( + async findAndPopulateWithCount( @Query(PageQueryPipe) pageQuery: PageQueryDto, @Query(PopulatePipe) populate: string[], @Query( @@ -134,7 +134,7 @@ export class NlpValueController extends BaseController< ) filters: TFilterQuery, ) { - return await this.nlpValueService.findAndPopulateNlpValuesWithCount( + return await this.nlpValueService.findAndPopulateWithCount( pageQuery, populate, filters, diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 8d68a654..54869623 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -108,13 +108,13 @@ export class NlpValueRepository extends BaseRepository< } } - async findAndPopulateNlpValuesWithCount( + async findAndPopulateWithCount( { limit = 10, skip = 0, sort = ['createdAt', -1] }: PageQueryDto, populate: string[], { $and = [], ...rest }: TFilterQuery, ) { return this.model - .aggregate([ + .aggregate([ { // support filters $match: { diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index 71ccd031..6e5ccc35 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -221,12 +221,12 @@ export class NlpValueService extends BaseService< return Promise.all(promises); } - async findAndPopulateNlpValuesWithCount( + async findAndPopulateWithCount( pageQuery: PageQueryDto, populate: string[], filters: TFilterQuery, ) { - return await this.repository.findAndPopulateNlpValuesWithCount( + return await this.repository.findAndPopulateWithCount( pageQuery, populate, filters, From 1eb09ab84ee413bb24442ee1e451c8cbad1094e2 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 26 Mar 2025 08:55:48 +0100 Subject: [PATCH 18/58] fix: update nlp value aggregate --- .../controllers/nlp-value.controller.spec.ts | 67 +---- .../nlp/controllers/nlp-value.controller.ts | 28 +-- .../nlp/repositories/nlp-value.repository.ts | 229 ++++++++++++------ api/src/nlp/schemas/nlp-value.schema.ts | 16 ++ api/src/nlp/services/nlp-value.service.ts | 18 +- 5 files changed, 185 insertions(+), 173 deletions(-) diff --git a/api/src/nlp/controllers/nlp-value.controller.spec.ts b/api/src/nlp/controllers/nlp-value.controller.spec.ts index 14277994..deedf872 100644 --- a/api/src/nlp/controllers/nlp-value.controller.spec.ts +++ b/api/src/nlp/controllers/nlp-value.controller.spec.ts @@ -10,18 +10,14 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { getUpdateOneError } from '@/utils/test/errors/messages'; -import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity'; import { installNlpValueFixtures, nlpValueFixtures, } from '@/utils/test/fixtures/nlpvalue'; -import { getPageQuery } from '@/utils/test/pagination'; import { closeInMongodConnection, rootMongooseTestModule, } from '@/utils/test/test'; -import { TFixtures } from '@/utils/test/types'; -import { buildTestingMocks } from '@/utils/test/utils'; import { NlpValueCreateDto } from '../dto/nlp-value.dto'; import { NlpEntityRepository } from '../repositories/nlp-entity.repository'; @@ -29,11 +25,7 @@ import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.rep import { NlpValueRepository } from '../repositories/nlp-value.repository'; import { NlpEntityModel } from '../schemas/nlp-entity.schema'; import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema'; -import { - NlpValue, - NlpValueFull, - NlpValueModel, -} from '../schemas/nlp-value.schema'; +import { NlpValue, NlpValueModel } from '../schemas/nlp-value.schema'; import { NlpEntityService } from '../services/nlp-entity.service'; import { NlpValueService } from '../services/nlp-value.service'; @@ -80,63 +72,6 @@ describe('NlpValueController', () => { afterEach(jest.clearAllMocks); - describe('findPage', () => { - it('should find nlp Values, and foreach nlp value populate the corresponding entity', async () => { - const pageQuery = getPageQuery({ - sort: ['value', 'desc'], - }); - const result = await nlpValueController.findPage( - pageQuery, - ['entity'], - {}, - ); - - const nlpValueFixturesWithEntities = nlpValueFixtures.reduce( - (acc, curr) => { - acc.push({ - ...curr, - entity: nlpEntityFixtures[ - parseInt(curr.entity!) - ] as NlpValueFull['entity'], - builtin: curr.builtin!, - expressions: curr.expressions!, - metadata: curr.metadata!, - }); - return acc; - }, - [] as TFixtures[], - ); - expect(result).toEqualPayload(nlpValueFixturesWithEntities); - }); - - it('should find nlp Values', async () => { - const pageQuery = getPageQuery({ - sort: ['value', 'desc'], - }); - const result = await nlpValueController.findPage( - pageQuery, - ['invalidCriteria'], - {}, - ); - const nlpEntities = await nlpEntityService.findAll(); - const nlpValueFixturesWithEntities = nlpValueFixtures.reduce( - (acc, curr) => { - const ValueWithEntities = { - ...curr, - entity: curr.entity ? nlpEntities[parseInt(curr.entity!)].id : null, - expressions: curr.expressions!, - metadata: curr.metadata!, - builtin: curr.builtin!, - }; - acc.push(ValueWithEntities); - return acc; - }, - [] as TFixtures[], - ); - expect(result).toEqualPayload(nlpValueFixturesWithEntities); - }); - }); - describe('count', () => { it('should count the nlp Values', async () => { const result = await nlpValueController.filterCount(); diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index 6c8c0e42..c7845f43 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -125,24 +125,8 @@ export class NlpValueController extends BaseController< return doc; } - @Get('') - async findAndPopulateWithCount( - @Query(PageQueryPipe) pageQuery: PageQueryDto, - @Query(PopulatePipe) populate: string[], - @Query( - new SearchFilterPipe({ allowedFields: ['entity', 'value'] }), - ) - filters: TFilterQuery, - ) { - return await this.nlpValueService.findAndPopulateWithCount( - pageQuery, - populate, - filters, - ); - } - /** - * Retrieves a paginated list of NLP values. + * Retrieves a paginated list of NLP values with NLP Samples count. * * Supports filtering, pagination, and optional population of related entities. * @@ -150,10 +134,10 @@ export class NlpValueController extends BaseController< * @param populate - An array of related entities to populate. * @param filters - Filters to apply when retrieving the NLP values. * - * @returns A promise resolving to a paginated list of NLP values. + * @returns A promise resolving to a paginated list of NLP values with NLP Samples count. */ - // @Get('') disabled - async findPage( + @Get() + async findWithCount( @Query(PageQueryPipe) pageQuery: PageQueryDto, @Query(PopulatePipe) populate: string[], @Query( @@ -164,8 +148,8 @@ export class NlpValueController extends BaseController< filters: TFilterQuery, ) { return this.canPopulate(populate) - ? await this.nlpValueService.findAndPopulate(filters, pageQuery) - : await this.nlpValueService.find(filters, pageQuery); + ? await this.nlpValueService.findAndPopulateWithCount(pageQuery, filters) + : await this.nlpValueService.findWithCount(pageQuery, filters); } /** diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 54869623..b6a5eec8 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -8,21 +8,27 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Document, Model, Query, Types } from 'mongoose'; +import { plainToClass } from 'class-transformer'; +import { Document, Model, PipelineStage, Query, Types } from 'mongoose'; import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { TFilterQuery } from '@/utils/types/filter.types'; import { NlpValueDto } from '../dto/nlp-value.dto'; +import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NLP_VALUE_POPULATE, NlpValue, NlpValueDocument, NlpValueFull, + NlpValueFullWithCount, NlpValuePopulate, + NlpValueWithCount, + TNlpValueCountFormat, } from '../schemas/nlp-value.schema'; +import { NlpEntityRepository } from './nlp-entity.repository'; import { NlpSampleEntityRepository } from './nlp-sample-entity.repository'; @Injectable() @@ -35,6 +41,8 @@ export class NlpValueRepository extends BaseRepository< constructor( @InjectModel(NlpValue.name) readonly model: Model, private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, + @Inject(forwardRef(() => NlpEntityRepository)) + private readonly nlpEntityRepository: NlpEntityRepository, ) { super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull); } @@ -108,97 +116,162 @@ export class NlpValueRepository extends BaseRepository< } } - async findAndPopulateWithCount( + private async aggregateWithCount( { limit = 10, skip = 0, sort = ['createdAt', -1] }: PageQueryDto, - populate: string[], { $and = [], ...rest }: TFilterQuery, + populatePipelineStages: PipelineStage[] = [], ) { - return this.model - .aggregate([ - { - // support filters - $match: { - ...rest, - ...($and.length && { - $and: - $and.map(({ entity, ...rest }) => - entity - ? { - ...rest, - entity: new Types.ObjectId(String(entity)), - } - : rest, - ) || [], - }), + const pipeline: PipelineStage[] = [ + // support pageQuery + { + $limit: limit, + }, + { + $skip: skip, + }, + { + $sort: { + [sort[0]]: sort[1] === 'desc' ? -1 : 1, + _id: sort[1] === 'desc' ? -1 : 1, + }, + }, + { + // support filters + $match: { + ...rest, + ...($and.length && { + $and: + $and.map(({ entity, ...rest }) => + entity + ? { + ...rest, + entity: new Types.ObjectId(String(entity)), + } + : rest, + ) || [], + }), + }, + }, + { + $lookup: { + from: 'nlpsampleentities', + localField: '_id', + foreignField: 'value', + as: 'sampleEntities', + }, + }, + { + $unwind: { + path: '$sampleEntities', + preserveNullAndEmptyArrays: true, + }, + }, + { + $group: { + _id: '$_id', + value: { $first: '$value' }, + expressions: { $first: '$expressions' }, + builtin: { $first: '$builtin' }, + metadata: { $first: '$metadata' }, + createdAt: { $first: '$createdAt' }, + updatedAt: { $first: '$updatedAt' }, + entity: { $first: '$entity' }, + nlpSamplesCount: { + $sum: { $cond: [{ $ifNull: ['$sampleEntities', false] }, 1, 0] }, }, }, - // support pageQuery - { - $limit: limit, - }, - { - $skip: skip, - }, - { - $sort: { - [sort[0]]: sort[1] === 'desc' ? -1 : 1, - }, - }, - { - $lookup: { - from: 'nlpsampleentities', - localField: '_id', - foreignField: 'value', - as: 'sampleEntities', - }, - }, - { - $unwind: { - path: '$sampleEntities', - preserveNullAndEmptyArrays: true, - }, + }, + { + $project: { + id: '$_id', + _id: 0, + value: 1, + expressions: 1, + builtin: 1, + entity: 1, + metadata: 1, + createdAt: 1, + updatedAt: 1, + nlpSamplesCount: 1, }, + }, + ...populatePipelineStages, + ]; + + return await this.model.aggregate>(pipeline).exec(); + } + + private async plainToClass( + format: 'full' | 'stub', + aggregatedResults: (NlpValueWithCount | NlpValueFullWithCount)[], + ): Promise[]> { + if (format === 'full') { + const nestedNlpEntities: NlpValueFullWithCount[] = []; + for (const { entity, ...rest } of aggregatedResults) { + const plainNlpValue = { + ...rest, + entity: plainToClass( + NlpEntity, + await this.nlpEntityRepository.findOne(entity), + { + excludePrefixes: ['_'], + }, + ), + }; + nestedNlpEntities.push( + plainToClass(NlpValueFullWithCount, plainNlpValue, { + excludePrefixes: ['_'], + }), + ); + } + return nestedNlpEntities as TNlpValueCountFormat[]; + } else { + const nestedNlpEntities: NlpValueWithCount[] = []; + for (const aggregatedResult of aggregatedResults) { + nestedNlpEntities.push( + plainToClass(NlpValueWithCount, aggregatedResult, { + excludePrefixes: ['_'], + }), + ); + } + return nestedNlpEntities as TNlpValueCountFormat[]; + } + } + + async findWithCount( + pageQuery: PageQueryDto, + filterQuery: TFilterQuery, + ): Promise { + const aggregatedResults = await this.aggregateWithCount<'stub'>( + pageQuery, + filterQuery, + ); + + return await this.plainToClass<'stub'>('stub', aggregatedResults); + } + + async findAndPopulateWithCount( + pageQuery: PageQueryDto, + filterQuery: TFilterQuery, + ): Promise { + const aggregatedResults = await this.aggregateWithCount<'full'>( + pageQuery, + filterQuery, + [ { $lookup: { from: 'nlpentities', localField: 'entity', foreignField: '_id', - as: 'entities', + as: 'entity', }, }, { - $group: { - _id: '$_id', - value: { $first: '$value' }, - expressions: { $first: '$expressions' }, - builtin: { $first: '$builtin' }, - metadata: { $first: '$metadata' }, - createdAt: { $first: '$createdAt' }, - updatedAt: { $first: '$updatedAt' }, - entity: { - // support populate - $first: this.canPopulate(populate) ? '$entities' : '$entity', - }, - nlpSamplesCount: { - $sum: { $cond: [{ $ifNull: ['$sampleEntities', false] }, 1, 0] }, - }, - }, + $unwind: '$entity', }, - { - $project: { - id: '$_id', - _id: 0, - value: 1, - expressions: 1, - builtin: 1, - entity: 1, - metadata: 1, - createdAt: 1, - updatedAt: 1, - nlpSamplesCount: 1, - }, - }, - ]) - .exec(); + ], + ); + + return await this.plainToClass<'full'>('full', aggregatedResults); } } diff --git a/api/src/nlp/schemas/nlp-value.schema.ts b/api/src/nlp/schemas/nlp-value.schema.ts index 523eaa3e..77edcae1 100644 --- a/api/src/nlp/schemas/nlp-value.schema.ts +++ b/api/src/nlp/schemas/nlp-value.schema.ts @@ -106,6 +106,18 @@ export class NlpValueFull extends NlpValueStub { entity: NlpEntity; } +export class NlpValueWithCount extends NlpValue { + nlpSamplesCount: number; +} + +export class NlpValueFullWithCount extends NlpValueFull { + nlpSamplesCount: number; +} + +export class NlpValueFullWithCountDto { + nlpSamplesCount: number; +} + export type NlpValueDocument = THydratedDocument; export const NlpValueModel: ModelDefinition = LifecycleHookManager.attach({ @@ -121,3 +133,7 @@ export type NlpValuePopulate = keyof TFilterPopulateFields< >; export const NLP_VALUE_POPULATE: NlpValuePopulate[] = ['entity']; + +export type TNlpValueCountFormat = T extends 'stub' + ? NlpValueWithCount + : NlpValueFullWithCount; diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index 6e5ccc35..23448440 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -19,7 +19,9 @@ import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NlpValue, NlpValueFull, + NlpValueFullWithCount, NlpValuePopulate, + NlpValueWithCount, } from '../schemas/nlp-value.schema'; import { NlpSampleEntityValue } from '../schemas/types'; @@ -221,15 +223,17 @@ export class NlpValueService extends BaseService< return Promise.all(promises); } + async findWithCount( + pageQuery: PageQueryDto, + filters: TFilterQuery, + ): Promise { + return await this.repository.findWithCount(pageQuery, filters); + } + async findAndPopulateWithCount( pageQuery: PageQueryDto, - populate: string[], filters: TFilterQuery, - ) { - return await this.repository.findAndPopulateWithCount( - pageQuery, - populate, - filters, - ); + ): Promise { + return await this.repository.findAndPopulateWithCount(pageQuery, filters); } } From 1cc99d00206dd08c12303fe28fe181cb98d2f26e Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 3 Apr 2025 08:17:43 +0100 Subject: [PATCH 19/58] fix: apply feedback updates --- .../nlp/repositories/nlp-value.repository.ts | 32 +++++++++++-------- .../components/nlp/components/NlpValue.tsx | 5 ++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index b6a5eec8..73236796 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -117,24 +117,15 @@ export class NlpValueRepository extends BaseRepository< } private async aggregateWithCount( - { limit = 10, skip = 0, sort = ['createdAt', -1] }: PageQueryDto, + { + limit = 10, + skip = 0, + sort = ['createdAt', 'desc'], + }: PageQueryDto, { $and = [], ...rest }: TFilterQuery, populatePipelineStages: PipelineStage[] = [], ) { const pipeline: PipelineStage[] = [ - // support pageQuery - { - $limit: limit, - }, - { - $skip: skip, - }, - { - $sort: { - [sort[0]]: sort[1] === 'desc' ? -1 : 1, - _id: sort[1] === 'desc' ? -1 : 1, - }, - }, { // support filters $match: { @@ -152,6 +143,13 @@ export class NlpValueRepository extends BaseRepository< }), }, }, + // support pageQuery + { + $skip: skip, + }, + { + $limit: limit, + }, { $lookup: { from: 'nlpsampleentities', @@ -196,6 +194,12 @@ export class NlpValueRepository extends BaseRepository< }, }, ...populatePipelineStages, + { + $sort: { + [sort[0]]: sort[1].toString().startsWith('desc') ? -1 : 1, + _id: sort[1].toString().startsWith('desc') ? -1 : 1, + }, + }, ]; return await this.model.aggregate>(pipeline).exec(); diff --git a/frontend/src/components/nlp/components/NlpValue.tsx b/frontend/src/components/nlp/components/NlpValue.tsx index 92cc7d04..c0a6b18d 100644 --- a/frontend/src/components/nlp/components/NlpValue.tsx +++ b/frontend/src/components/nlp/components/NlpValue.tsx @@ -126,14 +126,17 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { renderHeader, }, { - flex: 3, + flex: 2, field: "nlpSamplesCount", + align: "center", headerName: t("label.nlp_samples_count"), sortable: true, disableColumnMenu: true, + headerAlign: "center", renderHeader, renderCell: ({ row }) => ( Date: Thu, 3 Apr 2025 08:45:09 +0100 Subject: [PATCH 20/58] fix: update NlpValue to have full format --- frontend/src/components/nlp/components/NlpValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/nlp/components/NlpValue.tsx b/frontend/src/components/nlp/components/NlpValue.tsx index c0a6b18d..3c49543b 100644 --- a/frontend/src/components/nlp/components/NlpValue.tsx +++ b/frontend/src/components/nlp/components/NlpValue.tsx @@ -58,7 +58,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { $or: ["doc", "value"], }); const { dataGridProps } = useFind( - { entity: EntityType.NLP_VALUE }, + { entity: EntityType.NLP_VALUE, format: Format.FULL }, { params: searchPayload, }, From ebd2a66bbd4b4883f4e68d6b1b44de5c2e2f4852 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 3 Apr 2025 08:55:54 +0100 Subject: [PATCH 21/58] fix: apply feedbacks --- frontend/src/components/nlp/components/NlpValue.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/nlp/components/NlpValue.tsx b/frontend/src/components/nlp/components/NlpValue.tsx index 3c49543b..eb6b6be9 100644 --- a/frontend/src/components/nlp/components/NlpValue.tsx +++ b/frontend/src/components/nlp/components/NlpValue.tsx @@ -137,8 +137,8 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { renderCell: ({ row }) => ( ), From a38824a23a10bac9ede5530436e322e5eaff339d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 11:58:54 +0100 Subject: [PATCH 22/58] fix: resolve Circular dependency in unit tests --- .../controllers/nlp-value.controller.spec.ts | 1 + .../nlp/repositories/nlp-value.repository.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/src/nlp/controllers/nlp-value.controller.spec.ts b/api/src/nlp/controllers/nlp-value.controller.spec.ts index deedf872..a72b8571 100644 --- a/api/src/nlp/controllers/nlp-value.controller.spec.ts +++ b/api/src/nlp/controllers/nlp-value.controller.spec.ts @@ -18,6 +18,7 @@ import { closeInMongodConnection, rootMongooseTestModule, } from '@/utils/test/test'; +import { buildTestingMocks } from '@/utils/test/utils'; import { NlpValueCreateDto } from '../dto/nlp-value.dto'; import { NlpEntityRepository } from '../repositories/nlp-entity.repository'; diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 73236796..03f22af4 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -9,14 +9,20 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { plainToClass } from 'class-transformer'; -import { Document, Model, PipelineStage, Query, Types } from 'mongoose'; +import mongoose, { + Document, + Model, + PipelineStage, + Query, + Types, +} from 'mongoose'; import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { TFilterQuery } from '@/utils/types/filter.types'; import { NlpValueDto } from '../dto/nlp-value.dto'; -import { NlpEntity } from '../schemas/nlp-entity.schema'; +import { NlpEntity, NlpEntityModel } from '../schemas/nlp-entity.schema'; import { NLP_VALUE_POPULATE, NlpValue, @@ -28,7 +34,6 @@ import { TNlpValueCountFormat, } from '../schemas/nlp-value.schema'; -import { NlpEntityRepository } from './nlp-entity.repository'; import { NlpSampleEntityRepository } from './nlp-sample-entity.repository'; @Injectable() @@ -41,8 +46,6 @@ export class NlpValueRepository extends BaseRepository< constructor( @InjectModel(NlpValue.name) readonly model: Model, private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, - @Inject(forwardRef(() => NlpEntityRepository)) - private readonly nlpEntityRepository: NlpEntityRepository, ) { super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull); } @@ -216,7 +219,9 @@ export class NlpValueRepository extends BaseRepository< ...rest, entity: plainToClass( NlpEntity, - await this.nlpEntityRepository.findOne(entity), + await mongoose + .model(NlpEntityModel.name, NlpEntityModel.schema) + .findById(entity), { excludePrefixes: ['_'], }, From 18f26df7bb019c2fcced4ccfd411270fac5b43aa Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 12:04:40 +0100 Subject: [PATCH 23/58] fix: remove N/A defaultvalue --- frontend/src/components/nlp/components/NlpValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/nlp/components/NlpValue.tsx b/frontend/src/components/nlp/components/NlpValue.tsx index eb6b6be9..c6094204 100644 --- a/frontend/src/components/nlp/components/NlpValue.tsx +++ b/frontend/src/components/nlp/components/NlpValue.tsx @@ -138,7 +138,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { ), From 524f30576b16aefcac13b7815e65556245e340e0 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 12:07:02 +0100 Subject: [PATCH 24/58] fix: update Nlp Sample en translation --- frontend/public/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index b243c3ae..94124e91 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -334,7 +334,7 @@ "nlp": "NLU", "nlp_entity": "Entity", "nlp_entity_value": "Value", - "nlp_samples_count": "Nlp Samples count", + "nlp_samples_count": "Samples count", "value": "Value", "synonyms": "Synonyms", "lookups": "Lookups", From 74470ef8b83126672cb597964f1e4e1f60c46aab Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 4 Apr 2025 12:26:36 +0100 Subject: [PATCH 25/58] feat: handle backward curve drawing --- .../v2/AdvancedLink/AdvancedLinkFactory.tsx | 165 +++++++++++++++--- 1 file changed, 144 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index 7d9a6882..58dd3df9 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -11,6 +11,7 @@ import styled from "@emotion/styled"; import { DefaultLinkFactory, DefaultLinkWidget, + PortModel } from "@projectstorm/react-diagrams"; import { AdvancedLinkModel } from "./AdvancedLinkModel"; @@ -28,6 +29,112 @@ const createCurvedPath = (start: Point, end: Point, nodeHeight: number) => { return `M ${start.x},${start.y} C ${controlPoint1X},${controlPoint1Y} ${controlPoint2X},${controlPoint2Y} ${end.x},${end.y}`; }; +const createBackwardCurvedPath = ( + sourcePort: PortModel, + targetPort: PortModel, +) => { + const sourceNode = sourcePort.getNode(); + const targetNode = targetPort.getNode(); + // **NEW:** Get port dimensions for better alignment + const sourcePortSize = { width: sourcePort.width || 10, height: sourcePort.height || 10 }; + const targetPortSize = { width: targetPort.width || 10, height: targetPort.height || 10 }; + // Get node dimensions + const sourceNodeWidth = sourceNode.width; + const targetNodeWidth = targetNode.width; + const sourceNodeHeight = sourceNode.height; + const targetNodeHeight = targetNode.height; + // Get node boundaries + const sourceNodeBounds = { + left: sourceNode.getPosition().x, + right: sourceNode.getPosition().x + sourceNodeWidth, + top: sourceNode.getPosition().y, + bottom: sourceNode.getPosition().y + sourceNodeHeight, + }; + const targetNodeBounds = { + left: targetNode.getPosition().x, + right: targetNode.getPosition().x + targetNodeWidth, + top: targetNode.getPosition().y, + bottom: targetNode.getPosition().y +targetNodeHeight, + }; + // **NEW:** Adjust `start` and `end` to match the exact center of ports + const adjustedStart: Point = { + x: sourcePort.getPosition().x + sourcePortSize.width / 2, + y: sourcePort.getPosition().y + sourcePortSize.height / 2, + }; + const adjustedEnd: Point = { + x: targetPort.getPosition().x + targetPortSize.width / 2, + y: targetPort.getPosition().y + targetPortSize.height / 2, + }; + // Calculate the distance between nodes + const nodeDistance = Math.sqrt( + Math.pow(adjustedEnd.x - adjustedStart.x, 2) + Math.pow(adjustedEnd.y - adjustedStart.y, 2) + ); + // Logarithmic scaling function that adjusts between 1.5 and 2 based on distance + const logFactor = (distance) => { + const minDistance = 0.1; // A small value to prevent division by zero or too small values + const maxDistance = 2000; // A maximum value for nodeDistance where the function plateaus + // Logarithmic scale function to map distance to a factor between 1.5 and 2 + const scale = Math.log(distance + minDistance) / Math.log(maxDistance + minDistance); + + // Scale result to range between 1.5 and 2 + return 1.5 + scale * (2 - 1.5); + }; + // Use node dimensions and distance to calculate dynamic offsets + const horizontalOffset = Math.max(sourceNodeWidth, targetNodeWidth); + const verticalOffset = Math.max(sourceNodeHeight, targetNodeHeight); + + // Dynamic factor, adjusting horizontal and vertical offsets based on the distance + let adjustedHorizontalOffset = horizontalOffset* logFactor(nodeDistance); + + ; + let adjustedVerticalOffset = verticalOffset * logFactor(nodeDistance); + + // Horizontal overlap ratio (0 = no overlap, 1 = fully overlapping horizontally) + const xOverlapAmount = Math.max( + 0, + Math.min(sourceNodeBounds.right, targetNodeBounds.right) - + Math.max(sourceNodeBounds.left, targetNodeBounds.left) + ); + const maxXRange = Math.max(sourceNodeWidth, targetNodeWidth); + const xOverlapRatio = xOverlapAmount / maxXRange; + // Vertical overlap ratio (0 = no overlap, 1 = fully overlapping vertically) + const yOverlapAmount = Math.max( + 0, + Math.min(sourceNodeBounds.bottom, targetNodeBounds.bottom) - + Math.max(sourceNodeBounds.top, targetNodeBounds.top) + ); + const maxYRange = Math.max(sourceNodeHeight, targetNodeHeight); + const yOverlapRatio = yOverlapAmount / maxYRange; + // Determine vertical direction for Y alignment + const verticalDirection = adjustedEnd.y >= adjustedStart.y ? 1 : -1; + + // If Node Distance is small, multiply offsets by overlap ratios + // to avoid abrupt curve steepness + if (nodeDistance < 500) { + adjustedHorizontalOffset *= xOverlapRatio; + adjustedVerticalOffset *= yOverlapRatio; + } + // Compute control points with dynamic offset + let controlPoint1X = adjustedStart.x + adjustedHorizontalOffset; + let controlPoint1Y = adjustedStart.y + verticalDirection * adjustedVerticalOffset; + + let controlPoint2X = adjustedEnd.x - adjustedHorizontalOffset; + let controlPoint2Y = adjustedEnd.y - verticalDirection * adjustedVerticalOffset; + + controlPoint1X = Math.max(controlPoint1X, sourceNodeBounds.right + 10); + controlPoint2X = Math.min(controlPoint2X, targetNodeBounds.left - 10); + + controlPoint1Y = verticalDirection > 0 + ? Math.max(controlPoint1Y, sourceNodeBounds.bottom + 10) + : Math.min(controlPoint1Y, sourceNodeBounds.top - 10); + + controlPoint2Y = verticalDirection > 0 + ? Math.min(controlPoint2Y, targetNodeBounds.top - 10) + : Math.max(controlPoint2Y, targetNodeBounds.bottom + 10); + + // Return the cubic Bezier curve + return `M ${adjustedStart.x},${adjustedStart.y} C ${controlPoint1X},${controlPoint1Y} ${controlPoint2X},${controlPoint2Y} ${adjustedEnd.x},${adjustedEnd.y}`; +}; namespace S { export const Keyframes = keyframes` @@ -68,41 +175,57 @@ export class AdvancedLinkFactory extends DefaultLinkFactory { model: AdvancedLinkModel, selected: boolean, path: string, - ) { + ) { + + const sourcePort = model.getSourcePort(); + const targetPort = model.getTargetPort(); const isSelfLoop = - model.getSourcePort().getNode() === model.getTargetPort().getNode(); + sourcePort.getNode() === targetPort.getNode(); + const sourcePortPosition = sourcePort.getPosition(); + const targetPortPosition = targetPort.getPosition(); + const startPoint: Point = { + x: sourcePortPosition.x + 20, + y: sourcePortPosition.y + 20, + }; + const endPoint: Point = { + x: targetPortPosition.x + 20, + y: targetPortPosition.y + 20, + }; + // Check if it's a backward link (moving left) + const isBackward = startPoint.x - endPoint.x > 12; if (isSelfLoop) { - // Adjust the path to create a curve - const sourcePortPosition = model.getSourcePort().getPosition(); - const targetPortPosition = model.getTargetPort().getPosition(); - const startPoint: Point = { - x: sourcePortPosition.x + 20, - y: sourcePortPosition.y + 20, + const sourcePortSize = { width:sourcePort.width || 10, height:sourcePort.height || 10 }; + // Adjust start Point to match the exact source port's centre + const adjustedStartPoint: Point = { + x: sourcePortPosition.x + sourcePortSize.width / 2, + y: sourcePortPosition.y + sourcePortSize.height / 2, }; - const endPoint: Point = { - x: targetPortPosition.x + 20, - y: targetPortPosition.y + 20, - }; - const targetPortHeight = model.getTargetPort().height; + // Handle self-loop (curved) links + const targetPortHeight =targetPort.height; const targetNdeHeight = - (model.getTargetPort().getPosition().y - - model.getTargetPort().getNode().getPosition().y) * + (targetPort.getPosition().y - + targetPort.getNode().getPosition().y) * 2 + targetPortHeight; - path = createCurvedPath(startPoint, endPoint, targetNdeHeight); - } - - return ( + path = createCurvedPath(adjustedStartPoint, endPoint, targetNdeHeight); + } else if (isBackward) { + // Handle backward (leftward) link with refined function + path = createBackwardCurvedPath(sourcePort, targetPort); + } + +return ( ); } -} +} \ No newline at end of file From 26b6d96e5e0e31bc6acbd85d5764a51986ed0a88 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 4 Apr 2025 12:37:15 +0100 Subject: [PATCH 26/58] feat: add variable --- .../visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index 58dd3df9..75132914 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -33,6 +33,9 @@ const createBackwardCurvedPath = ( sourcePort: PortModel, targetPort: PortModel, ) => { + // Set a threshold for node proximity, below which dynamic adjustments to offsets are applied + // This helps in reducing abrupt curve steepness when nodes are close to each other + const proximityThreshold = 500; const sourceNode = sourcePort.getNode(); const targetNode = targetPort.getNode(); // **NEW:** Get port dimensions for better alignment @@ -110,7 +113,7 @@ const createBackwardCurvedPath = ( // If Node Distance is small, multiply offsets by overlap ratios // to avoid abrupt curve steepness - if (nodeDistance < 500) { + if (nodeDistance < proximityThreshold) { adjustedHorizontalOffset *= xOverlapRatio; adjustedVerticalOffset *= yOverlapRatio; } From 2daa30c453cc48911acd1fe70c578cb6b027124a Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 4 Apr 2025 12:44:59 +0100 Subject: [PATCH 27/58] fix: linting --- .../v2/AdvancedLink/AdvancedLinkFactory.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index 75132914..523c6896 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -57,7 +57,7 @@ const createBackwardCurvedPath = ( left: targetNode.getPosition().x, right: targetNode.getPosition().x + targetNodeWidth, top: targetNode.getPosition().y, - bottom: targetNode.getPosition().y +targetNodeHeight, + bottom: targetNode.getPosition().y + targetNodeHeight, }; // **NEW:** Adjust `start` and `end` to match the exact center of ports const adjustedStart: Point = { @@ -73,7 +73,7 @@ const createBackwardCurvedPath = ( Math.pow(adjustedEnd.x - adjustedStart.x, 2) + Math.pow(adjustedEnd.y - adjustedStart.y, 2) ); // Logarithmic scaling function that adjusts between 1.5 and 2 based on distance - const logFactor = (distance) => { + const logFactor = (distance: number) => { const minDistance = 0.1; // A small value to prevent division by zero or too small values const maxDistance = 2000; // A maximum value for nodeDistance where the function plateaus // Logarithmic scale function to map distance to a factor between 1.5 and 2 @@ -87,9 +87,7 @@ const createBackwardCurvedPath = ( const verticalOffset = Math.max(sourceNodeHeight, targetNodeHeight); // Dynamic factor, adjusting horizontal and vertical offsets based on the distance - let adjustedHorizontalOffset = horizontalOffset* logFactor(nodeDistance); - - ; + let adjustedHorizontalOffset = horizontalOffset * logFactor(nodeDistance); let adjustedVerticalOffset = verticalOffset * logFactor(nodeDistance); // Horizontal overlap ratio (0 = no overlap, 1 = fully overlapping horizontally) @@ -114,8 +112,8 @@ const createBackwardCurvedPath = ( // If Node Distance is small, multiply offsets by overlap ratios // to avoid abrupt curve steepness if (nodeDistance < proximityThreshold) { - adjustedHorizontalOffset *= xOverlapRatio; - adjustedVerticalOffset *= yOverlapRatio; + adjustedHorizontalOffset *= xOverlapRatio; + adjustedVerticalOffset *= yOverlapRatio; } // Compute control points with dynamic offset let controlPoint1X = adjustedStart.x + adjustedHorizontalOffset; @@ -205,7 +203,7 @@ export class AdvancedLinkFactory extends DefaultLinkFactory { y: sourcePortPosition.y + sourcePortSize.height / 2, }; // Handle self-loop (curved) links - const targetPortHeight =targetPort.height; + const targetPortHeight = targetPort.height; const targetNdeHeight = (targetPort.getPosition().y - targetPort.getNode().getPosition().y) * From 45d7e0a3ed3cb9581afbdcd6149aeee62698283e Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 4 Apr 2025 15:30:02 +0100 Subject: [PATCH 28/58] feat: decompose calculations into small functions --- .../v2/AdvancedLink/AdvancedLinkFactory.tsx | 262 ++++++++++++------ 1 file changed, 184 insertions(+), 78 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index 523c6896..779170b6 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -11,16 +11,163 @@ import styled from "@emotion/styled"; import { DefaultLinkFactory, DefaultLinkWidget, + NodeModel, PortModel } from "@projectstorm/react-diagrams"; import { AdvancedLinkModel } from "./AdvancedLinkModel"; +const PROXIMITY_THRESHOLD = 500; +const MIN_DISTANCE = 0.1; +const MAX_DISTANCE = 2000; +const CONTROL_POINT_PADDING = 10; + interface Point { x: number; y: number; } +interface Boundaries { + left: number, + right: number, + top: number, + bottom: number, + +} + +interface Dimensions { + width: number, + height: number, +} +// Helper function to get port dimensions +const getPortDimensions = (port: PortModel): Dimensions => { + return { + width: port.width || CONTROL_POINT_PADDING, + height: port.height || CONTROL_POINT_PADDING, + }; +}; +// Helper function to calculate port center point +const getPortCenterPoint = (port: PortModel): Point => { + const portSize = getPortDimensions(port); + + return { + x: port.getPosition().x + portSize.width / 2, + y: port.getPosition().y + portSize.height / 2, + }; +}; +/** + * Logarithmic scaling function that adjusts between 1.5 and 2 based on distance, + * minimum distance, and maximum distance. + * @param distance - The distance to scale. + * @param minDistance - A small value to prevent division by zero or too small values. + * @param maxDistance - The maximum expected distance. + */ +const logFactor = ( + distance: number, + minDistance: number, + maxDistance: number +): number => { + const scale = Math.log(distance + minDistance) / Math.log(maxDistance + minDistance); + + return 1.5 + scale * 0.5; // Scaled to range between 1.5 and 2 +}; +/** + * Calculates the horizontal (X-axis) overlap in pixels between two node boundaries. + * Returns 0 if there is no overlap. + */ +const calculateXOverlap = ( + sourceBounds: Boundaries, + targetBounds: Boundaries +): number => { + return Math.max( + 0, + Math.min(sourceBounds.right, targetBounds.right) - + Math.max(sourceBounds.left, targetBounds.left) + ); +}; +/** + * Calculates the vertical (Y-axis) overlap in pixels between two node boundaries. + * Returns 0 if there is no overlap. + */ +const calculateYOverlap = ( + sourceBounds: Boundaries, + targetBounds: Boundaries +): number => { + return Math.max( + 0, + Math.min(sourceBounds.bottom, targetBounds.bottom) - + Math.max(sourceBounds.top, targetBounds.top) + ); +}; +/** + * Converts an overlap amount into a ratio (0 to 1) based on the larger of the two node dimensions. + * Useful for dynamically adjusting offsets based on how much nodes visually intersect. + */ +const calculateOverlapRatio = ( + overlapAmount: number, + sourceDimension: number, + targetDimension: number +): number => { + const maxRange = Math.max(sourceDimension, targetDimension); + + return overlapAmount / maxRange; +}; +/** + * Computes the Euclidean distance between two points. + * Used to scale offsets and curve control points based on how far apart nodes are. + */ +const calculateDistance = (startPoint: Point, endPoint: Point): number => { + return Math.sqrt( + Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2) + ); +}; +/** + * Calculates the bounding box of a node based on its position and size. + * Returns an object with `left`, `right`, `top`, and `bottom` properties representing the node's edges. + */ +const calculateNodeBoundaries = (node: NodeModel): Boundaries => { + return { + left: node.getPosition().x, + right: node.getPosition().x + node.width, + top: node.getPosition().y, + bottom: node.getPosition().y + node.height, + }; +}; +/** + * Calculates a single control point for a cubic Bézier curve. + * Adjusts based on direction, dynamic offset, and node boundaries. + */ +const calculateControlPoint = ( + anchor: Point, + horizontalOffset: number, + verticalOffset: number, + verticalDirection: number, + nodeBounds: Boundaries, + isStart: boolean, + controlPointPadding: number +): Point => { + let x = + anchor.x + (isStart ? horizontalOffset : -horizontalOffset); + let y = + anchor.y + (isStart ? verticalDirection * verticalOffset : -verticalDirection * verticalOffset); + + // Apply minimum horizontal constraint + x = isStart + ? Math.max(x, nodeBounds.right + controlPointPadding) + : Math.min(x, nodeBounds.left - controlPointPadding); + + // Apply vertical constraint based on direction + y = + verticalDirection > 0 + ? isStart + ? Math.max(y, nodeBounds.bottom + controlPointPadding) + : Math.min(y, nodeBounds.top - controlPointPadding) + : isStart + ? Math.min(y, nodeBounds.top - controlPointPadding) + : Math.max(y, nodeBounds.bottom + controlPointPadding); + + return { x, y }; +}; const createCurvedPath = (start: Point, end: Point, nodeHeight: number) => { const controlPoint1X = start.x + nodeHeight - 20; const controlPoint1Y = start.y - nodeHeight; @@ -35,79 +182,40 @@ const createBackwardCurvedPath = ( ) => { // Set a threshold for node proximity, below which dynamic adjustments to offsets are applied // This helps in reducing abrupt curve steepness when nodes are close to each other - const proximityThreshold = 500; + const proximityThreshold = PROXIMITY_THRESHOLD; + const minDistance = MIN_DISTANCE; + const maxDistance = MAX_DISTANCE; const sourceNode = sourcePort.getNode(); const targetNode = targetPort.getNode(); - // **NEW:** Get port dimensions for better alignment - const sourcePortSize = { width: sourcePort.width || 10, height: sourcePort.height || 10 }; - const targetPortSize = { width: targetPort.width || 10, height: targetPort.height || 10 }; // Get node dimensions const sourceNodeWidth = sourceNode.width; const targetNodeWidth = targetNode.width; const sourceNodeHeight = sourceNode.height; const targetNodeHeight = targetNode.height; // Get node boundaries - const sourceNodeBounds = { - left: sourceNode.getPosition().x, - right: sourceNode.getPosition().x + sourceNodeWidth, - top: sourceNode.getPosition().y, - bottom: sourceNode.getPosition().y + sourceNodeHeight, - }; - const targetNodeBounds = { - left: targetNode.getPosition().x, - right: targetNode.getPosition().x + targetNodeWidth, - top: targetNode.getPosition().y, - bottom: targetNode.getPosition().y + targetNodeHeight, - }; + const sourceNodeBounds: Boundaries = calculateNodeBoundaries(sourceNode); + const targetNodeBounds: Boundaries = calculateNodeBoundaries(targetNode); // **NEW:** Adjust `start` and `end` to match the exact center of ports - const adjustedStart: Point = { - x: sourcePort.getPosition().x + sourcePortSize.width / 2, - y: sourcePort.getPosition().y + sourcePortSize.height / 2, - }; - const adjustedEnd: Point = { - x: targetPort.getPosition().x + targetPortSize.width / 2, - y: targetPort.getPosition().y + targetPortSize.height / 2, - }; + const adjustedStart: Point = getPortCenterPoint(sourcePort); + const adjustedEnd: Point = getPortCenterPoint(targetPort); // Calculate the distance between nodes - const nodeDistance = Math.sqrt( - Math.pow(adjustedEnd.x - adjustedStart.x, 2) + Math.pow(adjustedEnd.y - adjustedStart.y, 2) - ); - // Logarithmic scaling function that adjusts between 1.5 and 2 based on distance - const logFactor = (distance: number) => { - const minDistance = 0.1; // A small value to prevent division by zero or too small values - const maxDistance = 2000; // A maximum value for nodeDistance where the function plateaus - // Logarithmic scale function to map distance to a factor between 1.5 and 2 - const scale = Math.log(distance + minDistance) / Math.log(maxDistance + minDistance); - - // Scale result to range between 1.5 and 2 - return 1.5 + scale * (2 - 1.5); - }; + const nodeDistance: number = calculateDistance(adjustedStart, adjustedEnd); // Use node dimensions and distance to calculate dynamic offsets - const horizontalOffset = Math.max(sourceNodeWidth, targetNodeWidth); - const verticalOffset = Math.max(sourceNodeHeight, targetNodeHeight); + const horizontalOffset: number = Math.max(sourceNodeWidth, targetNodeWidth); + const verticalOffset: number = Math.max(sourceNodeHeight, targetNodeHeight); // Dynamic factor, adjusting horizontal and vertical offsets based on the distance - let adjustedHorizontalOffset = horizontalOffset * logFactor(nodeDistance); - let adjustedVerticalOffset = verticalOffset * logFactor(nodeDistance); + let adjustedHorizontalOffset: number = horizontalOffset * logFactor(nodeDistance, minDistance, maxDistance); + let adjustedVerticalOffset: number = verticalOffset * logFactor(nodeDistance, minDistance, maxDistance); // Horizontal overlap ratio (0 = no overlap, 1 = fully overlapping horizontally) - const xOverlapAmount = Math.max( - 0, - Math.min(sourceNodeBounds.right, targetNodeBounds.right) - - Math.max(sourceNodeBounds.left, targetNodeBounds.left) - ); - const maxXRange = Math.max(sourceNodeWidth, targetNodeWidth); - const xOverlapRatio = xOverlapAmount / maxXRange; + const xOverlapAmount: number = calculateXOverlap(sourceNodeBounds, targetNodeBounds); + const xOverlapRatio: number = calculateOverlapRatio(xOverlapAmount, sourceNodeWidth, targetNodeWidth); // Vertical overlap ratio (0 = no overlap, 1 = fully overlapping vertically) - const yOverlapAmount = Math.max( - 0, - Math.min(sourceNodeBounds.bottom, targetNodeBounds.bottom) - - Math.max(sourceNodeBounds.top, targetNodeBounds.top) - ); - const maxYRange = Math.max(sourceNodeHeight, targetNodeHeight); - const yOverlapRatio = yOverlapAmount / maxYRange; + const yOverlapAmount: number = calculateYOverlap(sourceNodeBounds, targetNodeBounds); + const yOverlapRatio: number = calculateOverlapRatio(yOverlapAmount, sourceNodeHeight, targetNodeHeight); // Determine vertical direction for Y alignment - const verticalDirection = adjustedEnd.y >= adjustedStart.y ? 1 : -1; + const verticalDirection: number = adjustedEnd.y >= adjustedStart.y ? 1 : -1; // If Node Distance is small, multiply offsets by overlap ratios // to avoid abrupt curve steepness @@ -116,25 +224,27 @@ const createBackwardCurvedPath = ( adjustedVerticalOffset *= yOverlapRatio; } // Compute control points with dynamic offset - let controlPoint1X = adjustedStart.x + adjustedHorizontalOffset; - let controlPoint1Y = adjustedStart.y + verticalDirection * adjustedVerticalOffset; - - let controlPoint2X = adjustedEnd.x - adjustedHorizontalOffset; - let controlPoint2Y = adjustedEnd.y - verticalDirection * adjustedVerticalOffset; - - controlPoint1X = Math.max(controlPoint1X, sourceNodeBounds.right + 10); - controlPoint2X = Math.min(controlPoint2X, targetNodeBounds.left - 10); - - controlPoint1Y = verticalDirection > 0 - ? Math.max(controlPoint1Y, sourceNodeBounds.bottom + 10) - : Math.min(controlPoint1Y, sourceNodeBounds.top - 10); - - controlPoint2Y = verticalDirection > 0 - ? Math.min(controlPoint2Y, targetNodeBounds.top - 10) - : Math.max(controlPoint2Y, targetNodeBounds.bottom + 10); + const controlPoint1 = calculateControlPoint( + adjustedStart, + adjustedHorizontalOffset, + adjustedVerticalOffset, + verticalDirection, + sourceNodeBounds, + true, + CONTROL_POINT_PADDING + ); + const controlPoint2 = calculateControlPoint( + adjustedEnd, + adjustedHorizontalOffset, + adjustedVerticalOffset, + verticalDirection, + targetNodeBounds, + false, + CONTROL_POINT_PADDING + ); // Return the cubic Bezier curve - return `M ${adjustedStart.x},${adjustedStart.y} C ${controlPoint1X},${controlPoint1Y} ${controlPoint2X},${controlPoint2Y} ${adjustedEnd.x},${adjustedEnd.y}`; + return `M ${adjustedStart.x},${adjustedStart.y} C ${controlPoint1.x},${controlPoint1.y} ${controlPoint2.x},${controlPoint2.y} ${adjustedEnd.x},${adjustedEnd.y}`; }; namespace S { @@ -196,12 +306,8 @@ export class AdvancedLinkFactory extends DefaultLinkFactory { const isBackward = startPoint.x - endPoint.x > 12; if (isSelfLoop) { - const sourcePortSize = { width:sourcePort.width || 10, height:sourcePort.height || 10 }; // Adjust start Point to match the exact source port's centre - const adjustedStartPoint: Point = { - x: sourcePortPosition.x + sourcePortSize.width / 2, - y: sourcePortPosition.y + sourcePortSize.height / 2, - }; + const adjustedStartPoint: Point = getPortCenterPoint(sourcePort); // Handle self-loop (curved) links const targetPortHeight = targetPort.height; const targetNdeHeight = From cae83e41aa4bb6a940b822fa99de7950718f6dfe Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 4 Apr 2025 15:32:21 +0100 Subject: [PATCH 29/58] fix: remove trailing whitespace --- .../visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index 779170b6..b5ec85de 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -32,7 +32,6 @@ interface Boundaries { right: number, top: number, bottom: number, - } interface Dimensions { @@ -109,7 +108,7 @@ const calculateOverlapRatio = ( targetDimension: number ): number => { const maxRange = Math.max(sourceDimension, targetDimension); - + return overlapAmount / maxRange; }; /** From 7a30c363fd5ff1d3ebbaef10ee3e7c3487ef0007 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 17:19:36 +0100 Subject: [PATCH 30/58] fix: update cleanup service methods description --- api/src/extension/cleanup.service.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/api/src/extension/cleanup.service.ts b/api/src/extension/cleanup.service.ts index 994ed5a3..c0b7355e 100644 --- a/api/src/extension/cleanup.service.ts +++ b/api/src/extension/cleanup.service.ts @@ -25,6 +25,14 @@ export class CleanupService { private readonly channelService: ChannelService, ) {} + /** + * Deletes unused settings with the specified criteria. + * + * @param criteria - An array of criteria objects containing: + * - suffix: Regex pattern to match setting groups + * - namespaces: Array of namespaces to exclude from deletion + * @returns A promise that resolves to the result of the deletion operation. + */ private async deleteManyBySuffixAndNamespaces( criteria: TCriteria[], ): Promise { @@ -35,19 +43,33 @@ export class CleanupService { }); } + /** + * Retrieves a list of channel Namespaces. + * + * @returns An array of channel Namespaces. + */ public getChannelNamespaces(): TExtractNamespace<'channel'>[] { return this.channelService .getAll() .map((channel) => channel.getNamespace>()); } + /** + * Retrieves a list of helper Namespaces. + * + * @returns An array of helper Namespaces. + */ public getHelperNamespaces(): TExtractNamespace<'helper'>[] { return this.helperService .getAll() .map((helper) => helper.getNamespace>()); } - public async pruneExtensionSettings() { + /** + * Prune extensions unused settings. + * + */ + public async pruneExtensionSettings(): Promise { const channels = this.getChannelNamespaces(); const helpers = this.getHelperNamespaces(); const { deletedCount } = await this.deleteManyBySuffixAndNamespaces([ From 563e337a6557bff9a5f487e9498a5dc1121c92d4 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 17:30:32 +0100 Subject: [PATCH 31/58] fix: remove unused array element --- api/src/extension/cleanup.service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/extension/cleanup.service.spec.ts b/api/src/extension/cleanup.service.spec.ts index 6b587ae0..f8e97ffb 100644 --- a/api/src/extension/cleanup.service.spec.ts +++ b/api/src/extension/cleanup.service.spec.ts @@ -74,7 +74,6 @@ describe('CleanupService', () => { CleanupService, SettingService, HelperService, - , ]); const [loggerService] = await resolveMocks([LoggerService]); From b51f5e85fbf89aa0434be35f591db17450de9b7b Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 4 Apr 2025 17:58:56 +0100 Subject: [PATCH 32/58] fix: apply feedback update --- api/src/extension/cleanup.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/extension/cleanup.service.ts b/api/src/extension/cleanup.service.ts index c0b7355e..62335ab0 100644 --- a/api/src/extension/cleanup.service.ts +++ b/api/src/extension/cleanup.service.ts @@ -38,7 +38,7 @@ export class CleanupService { ): Promise { return await this.settingService.deleteMany({ $or: criteria.map(({ suffix, namespaces }) => ({ - group: { $regex: suffix, $nin: namespaces }, + group: { $regex: new RegExp(`${suffix}$`), $nin: namespaces }, })), }); } From a3b92da470bce5a69bd0ee43aa7eeebc7ae64980 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sat, 5 Apr 2025 20:34:12 +0100 Subject: [PATCH 33/58] fix: update NlpEntity reponse to be plain --- api/src/nlp/repositories/nlp-value.repository.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 03f22af4..836990ce 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -221,7 +221,8 @@ export class NlpValueRepository extends BaseRepository< NlpEntity, await mongoose .model(NlpEntityModel.name, NlpEntityModel.schema) - .findById(entity), + .findById(entity) + .lean(), { excludePrefixes: ['_'], }, From e592a781b38ae66259ae236c02c4c9211aae5775 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sat, 5 Apr 2025 21:59:19 +0100 Subject: [PATCH 34/58] fix: refactor logic --- .../nlp/controllers/nlp-value.controller.ts | 9 +- .../nlp/repositories/nlp-value.repository.ts | 182 ++++++++++-------- api/src/nlp/schemas/nlp-value.schema.ts | 13 +- api/src/nlp/services/nlp-value.service.ts | 18 +- api/src/utils/types/format.types.ts | 18 ++ 5 files changed, 133 insertions(+), 107 deletions(-) create mode 100644 api/src/utils/types/format.types.ts diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index c7845f43..c9d98078 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -30,6 +30,7 @@ import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe'; import { PopulatePipe } from '@/utils/pipes/populate.pipe'; import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe'; import { TFilterQuery } from '@/utils/types/filter.types'; +import { Format } from '@/utils/types/format.types'; import { NlpValueCreateDto, NlpValueUpdateDto } from '../dto/nlp-value.dto'; import { @@ -147,9 +148,11 @@ export class NlpValueController extends BaseController< ) filters: TFilterQuery, ) { - return this.canPopulate(populate) - ? await this.nlpValueService.findAndPopulateWithCount(pageQuery, filters) - : await this.nlpValueService.findWithCount(pageQuery, filters); + return await this.nlpValueService.findWithCount( + this.canPopulate(populate) ? Format.FULL : Format.STUB, + pageQuery, + filters, + ); } /** diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 836990ce..5ae353b3 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -20,6 +20,7 @@ import mongoose, { import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { TFilterQuery } from '@/utils/types/filter.types'; +import { Format } from '@/utils/types/format.types'; import { NlpValueDto } from '../dto/nlp-value.dto'; import { NlpEntity, NlpEntityModel } from '../schemas/nlp-entity.schema'; @@ -31,7 +32,7 @@ import { NlpValueFullWithCount, NlpValuePopulate, NlpValueWithCount, - TNlpValueCountFormat, + TNlpValueCount, } from '../schemas/nlp-value.schema'; import { NlpSampleEntityRepository } from './nlp-sample-entity.repository'; @@ -119,31 +120,38 @@ export class NlpValueRepository extends BaseRepository< } } - private async aggregateWithCount( + /** + * Performs an aggregation to retrieve NLP values with their sample counts. + * + * @param pageQuery - The pagination parameters + * @param filterQuery - The filter criteria + * @param populatePipelineStages - Optional additional pipeline stages for populating related data + * @returns Aggregated results with sample counts + */ + private async aggregateWithCount( + format: F, { limit = 10, skip = 0, sort = ['createdAt', 'desc'], }: PageQueryDto, { $and = [], ...rest }: TFilterQuery, - populatePipelineStages: PipelineStage[] = [], - ) { + ): Promise[]> { const pipeline: PipelineStage[] = [ { // support filters $match: { ...rest, - ...($and.length && { - $and: - $and.map(({ entity, ...rest }) => - entity - ? { - ...rest, - entity: new Types.ObjectId(String(entity)), - } - : rest, - ) || [], - }), + ...($and.length + ? { + $and: $and.map(({ entity, ...rest }) => ({ + ...rest, + ...(entity + ? { entity: new Types.ObjectId(String(entity)) } + : {}), + })), + } + : {}), }, }, // support pageQuery @@ -196,92 +204,96 @@ export class NlpValueRepository extends BaseRepository< nlpSamplesCount: 1, }, }, - ...populatePipelineStages, + ...(format === Format.FULL + ? [ + { + $lookup: { + from: 'nlpentities', + localField: 'entity', + foreignField: '_id', + as: 'entity', + }, + }, + { + $unwind: '$entity', + }, + ] + : []), { $sort: { - [sort[0]]: sort[1].toString().startsWith('desc') ? -1 : 1, - _id: sort[1].toString().startsWith('desc') ? -1 : 1, + [sort[0]]: + typeof sort[1] === 'number' + ? sort[1] + : sort[1].toString().toLowerCase() === 'desc' + ? -1 + : 1, + _id: + typeof sort[1] === 'number' + ? sort[1] + : sort[1].toString().toLowerCase() === 'desc' + ? -1 + : 1, }, }, ]; - return await this.model.aggregate>(pipeline).exec(); + return await this.model.aggregate>(pipeline).exec(); } - private async plainToClass( - format: 'full' | 'stub', - aggregatedResults: (NlpValueWithCount | NlpValueFullWithCount)[], - ): Promise[]> { - if (format === 'full') { - const nestedNlpEntities: NlpValueFullWithCount[] = []; - for (const { entity, ...rest } of aggregatedResults) { - const plainNlpValue = { + private async plainToClass( + format: F, + aggregatedResults: TNlpValueCount[], + ): Promise[]> { + const result: typeof aggregatedResults = []; + + for (const item of aggregatedResults as TNlpValueCount[]) { + if (format === Format.FULL) { + const { entity, ...rest } = item; + const entityData = await mongoose + .model(NlpEntityModel.name, NlpEntityModel.schema) + .findById(entity) + .lean(); + + const plainNlpValue: NlpValueFull = { ...rest, - entity: plainToClass( - NlpEntity, - await mongoose - .model(NlpEntityModel.name, NlpEntityModel.schema) - .findById(entity) - .lean(), - { - excludePrefixes: ['_'], - }, - ), + entity: plainToClass(NlpEntity, entityData, { + excludePrefixes: ['_'], + }), }; - nestedNlpEntities.push( + + result.push( plainToClass(NlpValueFullWithCount, plainNlpValue, { excludePrefixes: ['_'], - }), + }) as TNlpValueCount, ); - } - return nestedNlpEntities as TNlpValueCountFormat[]; - } else { - const nestedNlpEntities: NlpValueWithCount[] = []; - for (const aggregatedResult of aggregatedResults) { - nestedNlpEntities.push( - plainToClass(NlpValueWithCount, aggregatedResult, { + } else { + result.push( + plainToClass(NlpValueWithCount, item, { excludePrefixes: ['_'], - }), + }) as TNlpValueCount, ); } - return nestedNlpEntities as TNlpValueCountFormat[]; + } + + return result; + } + + async findWithCount( + format: F, + pageQuery: PageQueryDto, + filterQuery: TFilterQuery, + ): Promise[]> { + try { + const aggregatedResults = await this.aggregateWithCount( + format, + pageQuery, + filterQuery, + ); + + return await this.plainToClass(format, aggregatedResults); + } catch (error) { + this.logger.error(`Error in findWithCount: ${error.message}`, error); + throw error; } } - - async findWithCount( - pageQuery: PageQueryDto, - filterQuery: TFilterQuery, - ): Promise { - const aggregatedResults = await this.aggregateWithCount<'stub'>( - pageQuery, - filterQuery, - ); - - return await this.plainToClass<'stub'>('stub', aggregatedResults); - } - - async findAndPopulateWithCount( - pageQuery: PageQueryDto, - filterQuery: TFilterQuery, - ): Promise { - const aggregatedResults = await this.aggregateWithCount<'full'>( - pageQuery, - filterQuery, - [ - { - $lookup: { - from: 'nlpentities', - localField: 'entity', - foreignField: '_id', - as: 'entity', - }, - }, - { - $unwind: '$entity', - }, - ], - ); - - return await this.plainToClass<'full'>('full', aggregatedResults); - } } diff --git a/api/src/nlp/schemas/nlp-value.schema.ts b/api/src/nlp/schemas/nlp-value.schema.ts index 77edcae1..a49eea5c 100644 --- a/api/src/nlp/schemas/nlp-value.schema.ts +++ b/api/src/nlp/schemas/nlp-value.schema.ts @@ -16,6 +16,7 @@ import { TFilterPopulateFields, THydratedDocument, } from '@/utils/types/filter.types'; +import { TStubOrFull } from '@/utils/types/format.types'; import { NlpEntity, NlpEntityFull } from './nlp-entity.schema'; import { NlpValueMap } from './types'; @@ -114,10 +115,6 @@ export class NlpValueFullWithCount extends NlpValueFull { nlpSamplesCount: number; } -export class NlpValueFullWithCountDto { - nlpSamplesCount: number; -} - export type NlpValueDocument = THydratedDocument; export const NlpValueModel: ModelDefinition = LifecycleHookManager.attach({ @@ -134,6 +131,8 @@ export type NlpValuePopulate = keyof TFilterPopulateFields< export const NLP_VALUE_POPULATE: NlpValuePopulate[] = ['entity']; -export type TNlpValueCountFormat = T extends 'stub' - ? NlpValueWithCount - : NlpValueFullWithCount; +export type TNlpValueCount = TStubOrFull< + T, + NlpValueWithCount, + NlpValueFullWithCount +>; diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index 23448440..87988140 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -12,6 +12,7 @@ import { DeleteResult } from '@/utils/generics/base-repository'; import { BaseService } from '@/utils/generics/base-service'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { TFilterQuery } from '@/utils/types/filter.types'; +import { Format } from '@/utils/types/format.types'; import { NlpValueCreateDto, NlpValueDto } from '../dto/nlp-value.dto'; import { NlpValueRepository } from '../repositories/nlp-value.repository'; @@ -19,9 +20,8 @@ import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NlpValue, NlpValueFull, - NlpValueFullWithCount, NlpValuePopulate, - NlpValueWithCount, + TNlpValueCount, } from '../schemas/nlp-value.schema'; import { NlpSampleEntityValue } from '../schemas/types'; @@ -223,17 +223,11 @@ export class NlpValueService extends BaseService< return Promise.all(promises); } - async findWithCount( + async findWithCount( + format: F, pageQuery: PageQueryDto, filters: TFilterQuery, - ): Promise { - return await this.repository.findWithCount(pageQuery, filters); - } - - async findAndPopulateWithCount( - pageQuery: PageQueryDto, - filters: TFilterQuery, - ): Promise { - return await this.repository.findAndPopulateWithCount(pageQuery, filters); + ): Promise[]> { + return await this.repository.findWithCount(format, pageQuery, filters); } } diff --git a/api/src/utils/types/format.types.ts b/api/src/utils/types/format.types.ts new file mode 100644 index 00000000..2336a6c1 --- /dev/null +++ b/api/src/utils/types/format.types.ts @@ -0,0 +1,18 @@ +/* + * 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 enum Format { + NONE = 0, + STUB = 1, + BASIC = 2, + FULL = 3, +} + +export type TStubOrFull = TF extends Format.STUB + ? TStub + : TFull; From f18c0618885a54d590dbae3dca9e67bd3b41801e Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sat, 5 Apr 2025 22:25:59 +0100 Subject: [PATCH 35/58] fix: update doc comment of aggregateWithCount method --- api/src/nlp/repositories/nlp-value.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 5ae353b3..05f716b0 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -123,10 +123,10 @@ export class NlpValueRepository extends BaseRepository< /** * Performs an aggregation to retrieve NLP values with their sample counts. * + * @param format - The format can be full or stub * @param pageQuery - The pagination parameters * @param filterQuery - The filter criteria - * @param populatePipelineStages - Optional additional pipeline stages for populating related data - * @returns Aggregated results with sample counts + * @returns Aggregated Nlp Value results with sample counts */ private async aggregateWithCount( format: F, From a729fe2f15fb0c40af1c209c6657280323458b5c Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sat, 5 Apr 2025 22:46:37 +0100 Subject: [PATCH 36/58] fix: update nlpEntity model injection --- .../nlp/repositories/nlp-value.repository.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 05f716b0..18632a3f 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -9,13 +9,7 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { plainToClass } from 'class-transformer'; -import mongoose, { - Document, - Model, - PipelineStage, - Query, - Types, -} from 'mongoose'; +import { Document, Model, PipelineStage, Query, Types } from 'mongoose'; import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; @@ -23,7 +17,7 @@ import { TFilterQuery } from '@/utils/types/filter.types'; import { Format } from '@/utils/types/format.types'; import { NlpValueDto } from '../dto/nlp-value.dto'; -import { NlpEntity, NlpEntityModel } from '../schemas/nlp-entity.schema'; +import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NLP_VALUE_POPULATE, NlpValue, @@ -47,6 +41,8 @@ export class NlpValueRepository extends BaseRepository< constructor( @InjectModel(NlpValue.name) readonly model: Model, private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, + @InjectModel(NlpEntity.name) + private readonly nlpEntityModel: Model, ) { super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull); } @@ -249,14 +245,11 @@ export class NlpValueRepository extends BaseRepository< for (const item of aggregatedResults as TNlpValueCount[]) { if (format === Format.FULL) { const { entity, ...rest } = item; - const entityData = await mongoose - .model(NlpEntityModel.name, NlpEntityModel.schema) - .findById(entity) - .lean(); + const nlpEntityData = await this.nlpEntityModel.findById(entity).lean(); const plainNlpValue: NlpValueFull = { ...rest, - entity: plainToClass(NlpEntity, entityData, { + entity: plainToClass(NlpEntity, nlpEntityData, { excludePrefixes: ['_'], }), }; From bdf92af8883595160b1c0f15b811af325ee4571f Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sat, 5 Apr 2025 23:04:46 +0100 Subject: [PATCH 37/58] fix: add getSortDirection method --- .../nlp/repositories/nlp-value.repository.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 18632a3f..60f6afea 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -9,7 +9,14 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { plainToClass } from 'class-transformer'; -import { Document, Model, PipelineStage, Query, Types } from 'mongoose'; +import { + Document, + Model, + PipelineStage, + Query, + SortOrder, + Types, +} from 'mongoose'; import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; @@ -217,18 +224,8 @@ export class NlpValueRepository extends BaseRepository< : []), { $sort: { - [sort[0]]: - typeof sort[1] === 'number' - ? sort[1] - : sort[1].toString().toLowerCase() === 'desc' - ? -1 - : 1, - _id: - typeof sort[1] === 'number' - ? sort[1] - : sort[1].toString().toLowerCase() === 'desc' - ? -1 - : 1, + [sort[0]]: this.getSortDirection(sort[1]), + _id: this.getSortDirection(sort[1]), }, }, ]; @@ -289,4 +286,12 @@ export class NlpValueRepository extends BaseRepository< throw error; } } + + private getSortDirection(sortOrder: SortOrder) { + return typeof sortOrder === 'number' + ? sortOrder + : sortOrder.toString().toLowerCase() === 'desc' + ? -1 + : 1; + } } From a6d9c9dae5b4bda5cd7b820b4ba7071928db0838 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 6 Apr 2025 00:07:56 +0100 Subject: [PATCH 38/58] fix: refactor plainToClass method --- .../nlp/repositories/nlp-value.repository.ts | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 60f6afea..4ac2efe1 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -8,7 +8,7 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { plainToClass } from 'class-transformer'; +import { ClassTransformOptions, plainToClass } from 'class-transformer'; import { Document, Model, @@ -48,8 +48,6 @@ export class NlpValueRepository extends BaseRepository< constructor( @InjectModel(NlpValue.name) readonly model: Model, private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, - @InjectModel(NlpEntity.name) - private readonly nlpEntityModel: Model, ) { super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull); } @@ -236,36 +234,28 @@ export class NlpValueRepository extends BaseRepository< private async plainToClass( format: F, aggregatedResults: TNlpValueCount[], + options: ClassTransformOptions = { excludePrefixes: ['_'] }, ): Promise[]> { - const result: typeof aggregatedResults = []; - - for (const item of aggregatedResults as TNlpValueCount[]) { + return aggregatedResults.map(({ entity, ...rest }) => { if (format === Format.FULL) { - const { entity, ...rest } = item; - const nlpEntityData = await this.nlpEntityModel.findById(entity).lean(); - - const plainNlpValue: NlpValueFull = { - ...rest, - entity: plainToClass(NlpEntity, nlpEntityData, { + return plainToClass( + NlpValueFullWithCount, + { + ...rest, + entity: plainToClass(NlpEntity, entity, options), + }, + { excludePrefixes: ['_'], - }), - }; - - result.push( - plainToClass(NlpValueFullWithCount, plainNlpValue, { - excludePrefixes: ['_'], - }) as TNlpValueCount, - ); + }, + ) as TNlpValueCount; } else { - result.push( - plainToClass(NlpValueWithCount, item, { - excludePrefixes: ['_'], - }) as TNlpValueCount, - ); + return plainToClass( + NlpValueWithCount, + { entity, ...rest }, + options, + ) as TNlpValueCount; } - } - - return result; + }); } async findWithCount( From 903ff0bbf91422892526dec7dedae83ab49fe3bb Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 6 Apr 2025 10:56:43 +0100 Subject: [PATCH 39/58] fix: update findWithCount method logic --- .../nlp/repositories/nlp-value.repository.ts | 56 ++++++------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 4ac2efe1..ab276904 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -8,7 +8,7 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { ClassTransformOptions, plainToClass } from 'class-transformer'; +import { plainToInstance } from 'class-transformer'; import { Document, Model, @@ -24,7 +24,6 @@ import { TFilterQuery } from '@/utils/types/filter.types'; import { Format } from '@/utils/types/format.types'; import { NlpValueDto } from '../dto/nlp-value.dto'; -import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NLP_VALUE_POPULATE, NlpValue, @@ -121,6 +120,14 @@ export class NlpValueRepository extends BaseRepository< } } + private getSortDirection(sortOrder: SortOrder) { + return typeof sortOrder === 'number' + ? sortOrder + : sortOrder.toString().toLowerCase() === 'desc' + ? -1 + : 1; + } + /** * Performs an aggregation to retrieve NLP values with their sample counts. * @@ -231,33 +238,6 @@ export class NlpValueRepository extends BaseRepository< return await this.model.aggregate>(pipeline).exec(); } - private async plainToClass( - format: F, - aggregatedResults: TNlpValueCount[], - options: ClassTransformOptions = { excludePrefixes: ['_'] }, - ): Promise[]> { - return aggregatedResults.map(({ entity, ...rest }) => { - if (format === Format.FULL) { - return plainToClass( - NlpValueFullWithCount, - { - ...rest, - entity: plainToClass(NlpEntity, entity, options), - }, - { - excludePrefixes: ['_'], - }, - ) as TNlpValueCount; - } else { - return plainToClass( - NlpValueWithCount, - { entity, ...rest }, - options, - ) as TNlpValueCount; - } - }); - } - async findWithCount( format: F, pageQuery: PageQueryDto, @@ -270,18 +250,18 @@ export class NlpValueRepository extends BaseRepository< filterQuery, ); - return await this.plainToClass(format, aggregatedResults); + if (format === Format.FULL) { + return plainToInstance(NlpValueFullWithCount, aggregatedResults, { + excludePrefixes: ['_'], + }) as TNlpValueCount[]; + } + + return plainToInstance(NlpValueWithCount, aggregatedResults, { + excludePrefixes: ['_'], + }) as TNlpValueCount[]; } catch (error) { this.logger.error(`Error in findWithCount: ${error.message}`, error); throw error; } } - - private getSortDirection(sortOrder: SortOrder) { - return typeof sortOrder === 'number' - ? sortOrder - : sortOrder.toString().toLowerCase() === 'desc' - ? -1 - : 1; - } } From 83849c2df2d68649e58e67ee6bd732e4f43eb97f Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 6 Apr 2025 11:46:37 +0100 Subject: [PATCH 40/58] fix: enhance aggregation pipeline --- .../nlp/repositories/nlp-value.repository.ts | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index ab276904..bab94d7c 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -147,7 +147,6 @@ export class NlpValueRepository extends BaseRepository< ): Promise[]> { const pipeline: PipelineStage[] = [ { - // support filters $match: { ...rest, ...($and.length @@ -162,7 +161,6 @@ export class NlpValueRepository extends BaseRepository< : {}), }, }, - // support pageQuery { $skip: skip, }, @@ -174,42 +172,34 @@ export class NlpValueRepository extends BaseRepository< from: 'nlpsampleentities', localField: '_id', foreignField: 'value', - as: 'sampleEntities', + as: '_sampleEntities', }, }, { $unwind: { - path: '$sampleEntities', + path: '$_sampleEntities', preserveNullAndEmptyArrays: true, }, }, { $group: { _id: '$_id', - value: { $first: '$value' }, - expressions: { $first: '$expressions' }, - builtin: { $first: '$builtin' }, - metadata: { $first: '$metadata' }, - createdAt: { $first: '$createdAt' }, - updatedAt: { $first: '$updatedAt' }, - entity: { $first: '$entity' }, + _originalDoc: { + $first: { + $unsetField: { input: '$$ROOT', field: 'nlpSamplesCount' }, + }, + }, nlpSamplesCount: { - $sum: { $cond: [{ $ifNull: ['$sampleEntities', false] }, 1, 0] }, + $sum: { $cond: [{ $ifNull: ['$_sampleEntities', false] }, 1, 0] }, }, }, }, { - $project: { - id: '$_id', - _id: 0, - value: 1, - expressions: 1, - builtin: 1, - entity: 1, - metadata: 1, - createdAt: 1, - updatedAt: 1, - nlpSamplesCount: 1, + $replaceWith: { + $mergeObjects: [ + '$_originalDoc', + { nlpSamplesCount: '$nlpSamplesCount' }, + ], }, }, ...(format === Format.FULL From 0d5678ce254f141a53243c68ce702d3801ea42b3 Mon Sep 17 00:00:00 2001 From: hexastack Date: Mon, 7 Apr 2025 08:24:50 +0100 Subject: [PATCH 41/58] fix: empty file when no dataset is selected --- frontend/src/components/nlp/components/NlpSample.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/nlp/components/NlpSample.tsx b/frontend/src/components/nlp/components/NlpSample.tsx index 8c068a4f..bde59faa 100644 --- a/frontend/src/components/nlp/components/NlpSample.tsx +++ b/frontend/src/components/nlp/components/NlpSample.tsx @@ -392,6 +392,7 @@ export default function NlpSample() { `nlpsample/export${type ? `?type=${type}` : ""}`, )} startIcon={} + disabled={dataGridProps?.rows?.length === 0} > {t("button.export")} From fb8d224e0c59b0f152be966a11f8504c89e8e9a0 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Mon, 7 Apr 2025 10:10:14 +0100 Subject: [PATCH 42/58] feat: apply feedback --- .../visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index b5ec85de..34920a54 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -309,13 +309,13 @@ export class AdvancedLinkFactory extends DefaultLinkFactory { const adjustedStartPoint: Point = getPortCenterPoint(sourcePort); // Handle self-loop (curved) links const targetPortHeight = targetPort.height; - const targetNdeHeight = + const targetNodeHeight = (targetPort.getPosition().y - targetPort.getNode().getPosition().y) * 2 + targetPortHeight; - path = createCurvedPath(adjustedStartPoint, endPoint, targetNdeHeight); + path = createCurvedPath(adjustedStartPoint, endPoint, targetNodeHeight); } else if (isBackward) { // Handle backward (leftward) link with refined function path = createBackwardCurvedPath(sourcePort, targetPort); From c68dde3c43098bd64f0f1fa5d461e2719cb0df7c Mon Sep 17 00:00:00 2001 From: hexastack Date: Mon, 7 Apr 2025 10:53:44 +0100 Subject: [PATCH 43/58] fix: add no result found message --- .../src/components/inbox/components/ConversationsList.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/inbox/components/ConversationsList.tsx b/frontend/src/components/inbox/components/ConversationsList.tsx index 66409f85..a4e95458 100644 --- a/frontend/src/components/inbox/components/ConversationsList.tsx +++ b/frontend/src/components/inbox/components/ConversationsList.tsx @@ -53,7 +53,7 @@ export const SubscribersList = (props: { </Grid> - {subscribers?.length > 0 && ( + {subscribers?.length > 0 ? ( <ConversationList scrollable loading={isFetching} @@ -87,6 +87,12 @@ export const SubscribersList = (props: { </Conversation> ))} </ConversationList> + ) : ( + <Grid paddingX={2}> + <p style={{ textAlign: "center", color: "#999", marginTop: "1rem" }}> + {t("message.no_result_found")} + </p> + </Grid> )} </> ); From c14970b7c638cf0764ceb4a5d3ad730830938760 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi <marrouchi.mohamed@.com> Date: Mon, 7 Apr 2025 11:41:10 +0100 Subject: [PATCH 44/58] feat: update node dimensions calculation --- .../v2/AdvancedLink/AdvancedLinkFactory.tsx | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index 34920a54..289edfeb 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -132,6 +132,31 @@ const calculateNodeBoundaries = (node: NodeModel): Boundaries => { bottom: node.getPosition().y + node.height, }; }; +/** + * Calculates the width and height of a node based on the position of one of its ports. + * + * This approach avoids relying on the node's width and height properties, + * which may not be accurate or available at render time due to asynchronous rendering behavior. + * + * Instead, it uses the relative position of the port to infer the size of the node. + * Assumes that the port's position reflects the visual layout and placement on the node. + * + * @param port - A PortModel instance attached to the node + * @returns An object containing the inferred width and height of the node + */ +const calculateNodeDimension = (port: PortModel): Dimensions => { + // Get the top-left position of the node + const nodePos = port.getNode().getPosition(); + // Get the top-left position of the port + const portPos = port.getPosition(); + // Width is the horizontal distance from the node's left to the port's right edge + const width = (portPos.x - nodePos.x) + port.width; + // Height is estimated by doubling the vertical offset from the node to the port + // (port is vertically centered), then adding the port's height + const height = Math.abs(portPos.y - nodePos.y) * 2 + port.height; + + return { width, height }; +}; /** * Calculates a single control point for a cubic Bézier curve. * Adjusts based on direction, dynamic offset, and node boundaries. @@ -187,10 +212,8 @@ const createBackwardCurvedPath = ( const sourceNode = sourcePort.getNode(); const targetNode = targetPort.getNode(); // Get node dimensions - const sourceNodeWidth = sourceNode.width; - const targetNodeWidth = targetNode.width; - const sourceNodeHeight = sourceNode.height; - const targetNodeHeight = targetNode.height; + const { width: sourceNodeWidth, height: sourceNodeHeight } = calculateNodeDimension(sourcePort); + const { width: targetNodeWidth, height: targetNodeHeight } = calculateNodeDimension(targetPort); // Get node boundaries const sourceNodeBounds: Boundaries = calculateNodeBoundaries(sourceNode); const targetNodeBounds: Boundaries = calculateNodeBoundaries(targetNode); From 3153ab09cb8078b03a840ee470ee9644224c2138 Mon Sep 17 00:00:00 2001 From: hexastack <talebhassan.ikbel@gmail.com> Date: Mon, 7 Apr 2025 11:53:39 +0100 Subject: [PATCH 45/58] fix: importing csv file ignores fields and status always true --- api/src/cms/services/content.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/cms/services/content.service.ts b/api/src/cms/services/content.service.ts index 2d43dfc5..6f0ef26a 100644 --- a/api/src/cms/services/content.service.ts +++ b/api/src/cms/services/content.service.ts @@ -145,7 +145,7 @@ export class ContentService extends BaseService< ...acc, { title: String(title), - status: Boolean(status), + status: status.trim().toLowerCase() === 'true', entity: targetContentType, dynamicFields: Object.keys(rest) .filter((key) => From 7f3d53eed95c54a04812845d1507269015325af4 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi <marrouchi.mohamed@.com> Date: Mon, 7 Apr 2025 11:56:48 +0100 Subject: [PATCH 46/58] feat: apply coderabbit suggestions --- .../v2/AdvancedLink/AdvancedLinkFactory.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx index 289edfeb..242dcb84 100644 --- a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLinkFactory.tsx @@ -21,6 +21,9 @@ const PROXIMITY_THRESHOLD = 500; const MIN_DISTANCE = 0.1; const MAX_DISTANCE = 2000; const CONTROL_POINT_PADDING = 10; +const BACKWARD_LINK_THRESHOLD = 12; // pixels +const MIN_SCALE_FACTOR = 1.5; +const MAX_SCALE_FACTOR = 2.0; interface Point { x: number; @@ -68,7 +71,7 @@ const logFactor = ( ): number => { const scale = Math.log(distance + minDistance) / Math.log(maxDistance + minDistance); - return 1.5 + scale * 0.5; // Scaled to range between 1.5 and 2 + return MIN_SCALE_FACTOR + scale * (MAX_SCALE_FACTOR - MIN_SCALE_FACTOR); // Scaled to range between 1.5 and 2 }; /** * Calculates the horizontal (X-axis) overlap in pixels between two node boundaries. @@ -309,11 +312,10 @@ export class AdvancedLinkFactory extends DefaultLinkFactory { selected: boolean, path: string, ) { - + const backwardLinkThreshold = BACKWARD_LINK_THRESHOLD; const sourcePort = model.getSourcePort(); const targetPort = model.getTargetPort(); - const isSelfLoop = - sourcePort.getNode() === targetPort.getNode(); + const isSelfLoop = sourcePort.getNode() === targetPort.getNode(); const sourcePortPosition = sourcePort.getPosition(); const targetPortPosition = targetPort.getPosition(); const startPoint: Point = { @@ -325,7 +327,7 @@ export class AdvancedLinkFactory extends DefaultLinkFactory { y: targetPortPosition.y + 20, }; // Check if it's a backward link (moving left) - const isBackward = startPoint.x - endPoint.x > 12; + const isBackward = startPoint.x - endPoint.x > backwardLinkThreshold; if (isSelfLoop) { // Adjust start Point to match the exact source port's centre From 551c3ccf3529155b522a2a424f92ce5d8bfa19eb Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Mon, 7 Apr 2025 14:38:36 +0100 Subject: [PATCH 47/58] fix: add missing content payloadType --- api/src/chat/schemas/types/button.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/chat/schemas/types/button.ts b/api/src/chat/schemas/types/button.ts index 8391c41b..5649d56d 100644 --- a/api/src/chat/schemas/types/button.ts +++ b/api/src/chat/schemas/types/button.ts @@ -42,4 +42,5 @@ export enum PayloadType { button = 'button', outcome = 'outcome', menu = 'menu', + content = 'content', } From b96048c7ee8087f5e33c22f6ff189094b4b34dd0 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Mon, 7 Apr 2025 14:46:45 +0100 Subject: [PATCH 48/58] fix: add content payloadType unit test --- .../chat/controllers/block.controller.spec.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/src/chat/controllers/block.controller.spec.ts b/api/src/chat/controllers/block.controller.spec.ts index 85569e9c..24945c3d 100644 --- a/api/src/chat/controllers/block.controller.spec.ts +++ b/api/src/chat/controllers/block.controller.spec.ts @@ -361,4 +361,30 @@ describe('BlockController', () => { ).toBeDefined(); expect(result.patterns).toEqual(updateBlock.patterns); }); + + it('should update the block trigger with a content payloadType payload', async () => { + jest.spyOn(blockService, 'updateOne'); + const updateBlock: BlockUpdateDto = { + patterns: [ + { + label: 'Content label', + value: 'Content value', + type: PayloadType.content, + }, + ], + }; + const result = await blockController.updateOne(block.id, updateBlock); + expect(blockService.updateOne).toHaveBeenCalledWith(block.id, updateBlock); + + expect( + result.patterns.find( + (pattern) => + typeof pattern === 'object' && + 'type' in pattern && + pattern.type === PayloadType.content && + pattern, + ), + ).toBeDefined(); + expect(result.patterns).toEqual(updateBlock.patterns); + }); }); From 7888252e323742b4550fba58473cee05a22ea2fe Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Tue, 8 Apr 2025 17:50:44 +0100 Subject: [PATCH 49/58] fix(frontend): align BlockFormProvider method prop and the useForm typing --- .../src/components/visual-editor/form/BlockFormProvider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/visual-editor/form/BlockFormProvider.tsx b/frontend/src/components/visual-editor/form/BlockFormProvider.tsx index 043749a4..f44979dd 100644 --- a/frontend/src/components/visual-editor/form/BlockFormProvider.tsx +++ b/frontend/src/components/visual-editor/form/BlockFormProvider.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. @@ -9,7 +9,7 @@ import { createContext, ReactNode, useContext } from "react"; import { FormProvider, UseFormReturn } from "react-hook-form"; -import { IBlockAttributes, IBlock } from "@/types/block.types"; +import { IBlock, IBlockAttributes } from "@/types/block.types"; // Create a custom context for the block value const BlockContext = createContext<IBlock | undefined>(undefined); @@ -23,7 +23,7 @@ function BlockFormProvider({ methods, block, }: { - methods: UseFormReturn<IBlockAttributes, any, undefined>; + methods: UseFormReturn<IBlockAttributes>; block: IBlock | undefined; children: ReactNode; }) { From 75a69a3260e7b51b46a8ff9ee5020afbf5d02a36 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Thu, 10 Apr 2025 09:13:19 +0100 Subject: [PATCH 50/58] fix(frontend): update dependencies category --- frontend/package.json | 2 +- package-lock.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f814932b..7c7c2d4c 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,6 @@ "@mui/x-data-grid": "^7.3.2", "@projectstorm/react-canvas-core": "^7.0.3", "@projectstorm/react-diagrams": "^7.0.4", - "@types/qs": "^6.9.15", "axios": "^1.7.7", "eazychart-css": "^0.2.1-alpha.0", "eazychart-react": "^0.8.0-alpha.0", @@ -47,6 +46,7 @@ "socket.io-client": "^4.7.5" }, "devDependencies": { + "@types/qs": "^6.9.15", "@types/node": "20.12.12", "@types/random-seed": "^0.3.5", "@types/react": "18.3.2", diff --git a/package-lock.json b/package-lock.json index b6e94e60..a14be9a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,6 @@ "@mui/x-data-grid": "^7.3.2", "@projectstorm/react-canvas-core": "^7.0.3", "@projectstorm/react-diagrams": "^7.0.4", - "@types/qs": "^6.9.15", "axios": "^1.7.7", "eazychart-css": "^0.2.1-alpha.0", "eazychart-react": "^0.8.0-alpha.0", @@ -79,6 +78,7 @@ }, "devDependencies": { "@types/node": "20.12.12", + "@types/qs": "^6.9.15", "@types/random-seed": "^0.3.5", "@types/react": "18.3.2", "@types/react-dom": "^18", @@ -2877,6 +2877,7 @@ "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true, "license": "MIT" }, "node_modules/@types/random-seed": { From 5e4493d2faafd6ce2e74669f23afe732166fd32f Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Thu, 10 Apr 2025 09:14:05 +0100 Subject: [PATCH 51/58] fix(widget): update dependencies category --- package-lock.json | 3 ++- widget/package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a14be9a5..6587d1b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2824,6 +2824,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/@types/emoji-js/-/emoji-js-3.5.2.tgz", "integrity": "sha512-qPR85yjSPk2UEbdjYYNHfcOjVod7DCARSrJlPcL+cwaDFwdnmOFhPyYUvP5GaW0YZEy8mU93ZjTNgsVWz1zzlg==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree": { @@ -10308,7 +10309,6 @@ "version": "2.2.5", "license": "AGPL-3.0-only", "dependencies": { - "@types/emoji-js": "^3.5.2", "autolinker": "^4.0.0", "dayjs": "^1.11.12", "emoji-js": "^3.8.0", @@ -10318,6 +10318,7 @@ "socket.io-client": "^4.7.5" }, "devDependencies": { + "@types/emoji-js": "^3.5.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.15.0", diff --git a/widget/package.json b/widget/package.json index e7f4679b..bc191abd 100644 --- a/widget/package.json +++ b/widget/package.json @@ -20,7 +20,6 @@ "*.{ts,tsx}": "eslint --fix -c \".eslintrc-staged.json\"" }, "dependencies": { - "@types/emoji-js": "^3.5.2", "autolinker": "^4.0.0", "dayjs": "^1.11.12", "emoji-js": "^3.8.0", @@ -30,6 +29,7 @@ "socket.io-client": "^4.7.5" }, "devDependencies": { + "@types/emoji-js": "^3.5.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.15.0", From 3eb998c4a2a96dea1eb53f1ac7610c15b90f1ab7 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Thu, 10 Apr 2025 09:45:19 +0100 Subject: [PATCH 52/58] fix(api): update dependencies category --- api/package-lock.json | 2 +- api/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 1abb8898..c809c2cd 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -32,7 +32,6 @@ "cache-manager-redis-yet": "^4.1.2", "connect-mongo": "^5.1.0", "cookie-parser": "^1.4.6", - "dotenv": "^16.3.1", "express-session": "^1.17.3", "handlebars": "^4.7.8", "module-alias": "^2.2.3", @@ -86,6 +85,7 @@ "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "dotenv": "^16.3.1", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "~3.6.1", diff --git a/api/package.json b/api/package.json index 2f514805..a08ddba7 100644 --- a/api/package.json +++ b/api/package.json @@ -67,7 +67,6 @@ "cache-manager-redis-yet": "^4.1.2", "connect-mongo": "^5.1.0", "cookie-parser": "^1.4.6", - "dotenv": "^16.3.1", "express-session": "^1.17.3", "handlebars": "^4.7.8", "module-alias": "^2.2.3", @@ -121,6 +120,7 @@ "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "dotenv": "^16.3.1", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "~3.6.1", From 96f6ab55f78a5274d8c2a69f67693c4c90ca5aff Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Thu, 10 Apr 2025 15:36:21 +0100 Subject: [PATCH 53/58] fix: make plugin settings translatable --- api/src/i18n/services/translation.service.ts | 39 +++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/api/src/i18n/services/translation.service.ts b/api/src/i18n/services/translation.service.ts index c2adddd8..7ffc66ec 100644 --- a/api/src/i18n/services/translation.service.ts +++ b/api/src/i18n/services/translation.service.ts @@ -12,6 +12,7 @@ import { OnEvent } from '@nestjs/event-emitter'; import { I18nService } from '@/i18n/services/i18n.service'; import { PluginService } from '@/plugins/plugins.service'; import { PluginType } from '@/plugins/types'; +import { SettingType } from '@/setting/schemas/types'; import { SettingService } from '@/setting/services/setting.service'; import { BaseService } from '@/utils/generics/base-service'; @@ -57,21 +58,33 @@ export class TranslationService extends BaseService<Translation> { PluginType.block, block.message.plugin, ); - const defaultSettings = await plugin?.getDefaultSettings(); + const defaultSettings = (await plugin?.getDefaultSettings()) || []; + const filteredSettings = defaultSettings.filter( + ({ translatable, type }) => + [ + SettingType.text, + SettingType.textarea, + SettingType.multiple_text, + ].includes(type) && + (translatable === undefined || translatable === true), + ); + const settingTypeMap = new Map( + filteredSettings.map((setting) => [setting.label, setting.type]), + ); - // plugin - Object.entries(block.message.args).forEach(([l, arg]) => { - const setting = defaultSettings?.find(({ label }) => label === l); - if (setting?.translatable) { - if (Array.isArray(arg)) { - // array of text - strings = strings.concat(arg); - } else if (typeof arg === 'string') { - // text - strings.push(arg); - } + for (const [key, value] of Object.entries(block.message.args)) { + switch (settingTypeMap.get(key)) { + case SettingType.multiple_text: + strings = strings.concat(value); + break; + case SettingType.text: + case SettingType.textarea: + strings.push(value); + break; + default: + break; } - }); + } } else if ('text' in block.message && Array.isArray(block.message.text)) { // array of text strings = strings.concat(block.message.text); From 3d1c3f43286959716e8a091e7a9dffe174708d10 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Thu, 10 Apr 2025 15:38:26 +0100 Subject: [PATCH 54/58] fix: add a settingType variable --- api/src/i18n/services/translation.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/i18n/services/translation.service.ts b/api/src/i18n/services/translation.service.ts index 7ffc66ec..64a6a1c0 100644 --- a/api/src/i18n/services/translation.service.ts +++ b/api/src/i18n/services/translation.service.ts @@ -73,7 +73,9 @@ export class TranslationService extends BaseService<Translation> { ); for (const [key, value] of Object.entries(block.message.args)) { - switch (settingTypeMap.get(key)) { + const settingType = settingTypeMap.get(key); + + switch (settingType) { case SettingType.multiple_text: strings = strings.concat(value); break; From e46a86b586e96a9fb70899dcad0eb0300a42fc10 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Fri, 11 Apr 2025 08:30:08 +0100 Subject: [PATCH 55/58] fix: add exception when useBlock is outside the block provider --- .../visual-editor/form/BlockFormProvider.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/visual-editor/form/BlockFormProvider.tsx b/frontend/src/components/visual-editor/form/BlockFormProvider.tsx index 043749a4..d98c9b6c 100644 --- a/frontend/src/components/visual-editor/form/BlockFormProvider.tsx +++ b/frontend/src/components/visual-editor/form/BlockFormProvider.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. @@ -9,13 +9,21 @@ import { createContext, ReactNode, useContext } from "react"; import { FormProvider, UseFormReturn } from "react-hook-form"; -import { IBlockAttributes, IBlock } from "@/types/block.types"; +import { IBlock, IBlockAttributes } from "@/types/block.types"; // Create a custom context for the block value const BlockContext = createContext<IBlock | undefined>(undefined); // Custom hook to use block context -export const useBlock = () => useContext(BlockContext); +export const useBlock = () => { + const context = useContext(BlockContext); + + if (!context) { + throw new Error("useBlock must be used within an BlockContext"); + } + + return context; +}; // This component wraps FormProvider and adds block to its context function BlockFormProvider({ From d7cfca761bf879e4fb2b1795a0c88a5577496777 Mon Sep 17 00:00:00 2001 From: abdou6666 <sfayhi.abderrahmen@esprit.tn> Date: Fri, 11 Apr 2025 18:46:30 +0100 Subject: [PATCH 56/58] fix: dependencies --- frontend/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f814932b..73c43655 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,6 @@ "@mui/x-data-grid": "^7.3.2", "@projectstorm/react-canvas-core": "^7.0.3", "@projectstorm/react-diagrams": "^7.0.4", - "@types/qs": "^6.9.15", "axios": "^1.7.7", "eazychart-css": "^0.2.1-alpha.0", "eazychart-react": "^0.8.0-alpha.0", @@ -44,7 +43,8 @@ "react-hook-form": "^7.51.5", "react-i18next": "^14.1.1", "react-query": "^3.39.3", - "socket.io-client": "^4.7.5" + "socket.io-client": "^4.7.5", + "random-seed": "^0.3.0" }, "devDependencies": { "@types/node": "20.12.12", @@ -57,8 +57,8 @@ "eslint-import-resolver-typescript": "~3.6.1", "eslint-plugin-header": "^3.1.1", "lint-staged": "^15.3.0", - "random-seed": "^0.3.0", - "typescript": "^5.5.3" + "typescript": "^5.5.3", + "@types/qs": "^6.9.15" }, "engines": { "node": "^18.17.0" From f12f1de767a713ff3b23aa1312b256c4a3e32566 Mon Sep 17 00:00:00 2001 From: Mohamed Chedli Ben Yaghlane <3912825+medchedli@users.noreply.github.com> Date: Fri, 11 Apr 2025 20:49:44 +0100 Subject: [PATCH 57/58] Update frontend/src/components/inbox/components/ConversationsList.tsx Co-authored-by: Yassine <95612053+yassinedorbozgithub@users.noreply.github.com> --- .../src/components/inbox/components/ConversationsList.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/inbox/components/ConversationsList.tsx b/frontend/src/components/inbox/components/ConversationsList.tsx index a4e95458..14a0bcdc 100644 --- a/frontend/src/components/inbox/components/ConversationsList.tsx +++ b/frontend/src/components/inbox/components/ConversationsList.tsx @@ -88,10 +88,8 @@ export const SubscribersList = (props: { ))} </ConversationList> ) : ( - <Grid paddingX={2}> - <p style={{ textAlign: "center", color: "#999", marginTop: "1rem" }}> - {t("message.no_result_found")} - </p> + <Grid p={1} color="gray" textAlign="center"> + {t("message.no_result_found")} </Grid> )} </> From 01e570c440fa3f5f83a5bc86ecaa8672365a5300 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub <yassine.dorboz.2022@gmail.com> Date: Fri, 11 Apr 2025 23:44:39 +0100 Subject: [PATCH 58/58] fix: resolve file conflicts --- frontend/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 788f512b..04c5613e 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,8 +58,7 @@ "eslint-import-resolver-typescript": "~3.6.1", "eslint-plugin-header": "^3.1.1", "lint-staged": "^15.3.0", - "typescript": "^5.5.3", - "@types/qs": "^6.9.15" + "typescript": "^5.5.3" }, "engines": { "node": "^18.17.0"