mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge branch 'main' into fix/cleanup-languageId-nlp-sample
This commit is contained in:
2
api/package-lock.json
generated
2
api/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -93,8 +93,9 @@ export class BotStatsService extends BaseService<BotStats> {
|
||||
) {
|
||||
this.eventEmitter.emit(
|
||||
'hook:stats:entry',
|
||||
'retention',
|
||||
BotStatsType.retention,
|
||||
'Retentioned users',
|
||||
subscriber,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +107,11 @@ export class BotStatsService extends BaseService<BotStats> {
|
||||
* @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<void> {
|
||||
async handleStatEntry(
|
||||
type: BotStatsType,
|
||||
name: string,
|
||||
_subscriber: Subscriber,
|
||||
): Promise<void> {
|
||||
const day = new Date();
|
||||
day.setMilliseconds(0);
|
||||
day.setSeconds(0);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,42 +171,38 @@ describe('BlockRepository', () => {
|
||||
|
||||
describe('prepareBlocksInCategoryUpdateScope', () => {
|
||||
it('should update blocks within the scope based on category and ids', async () => {
|
||||
jest.spyOn(blockRepository, 'findOne').mockResolvedValue({
|
||||
id: blockValidIds[0],
|
||||
category: 'oldCategory',
|
||||
nextBlocks: [blockValidIds[1]],
|
||||
attachedBlock: blockValidIds[1],
|
||||
} as Block);
|
||||
|
||||
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
|
||||
jest.spyOn(blockRepository, 'find').mockResolvedValue([
|
||||
{
|
||||
id: blockValidIds[0],
|
||||
category: category.id,
|
||||
nextBlocks: [blockValidIds[1]],
|
||||
attachedBlock: blockValidIds[2],
|
||||
},
|
||||
] as Block[]);
|
||||
jest.spyOn(blockRepository, 'updateOne');
|
||||
|
||||
await blockRepository.prepareBlocksInCategoryUpdateScope(
|
||||
validCategory,
|
||||
blockValidIds,
|
||||
);
|
||||
|
||||
expect(mockUpdateOne).toHaveBeenCalledWith(blockValidIds[0], {
|
||||
expect(blockRepository.updateOne).toHaveBeenCalledWith(blockValidIds[0], {
|
||||
nextBlocks: [blockValidIds[1]],
|
||||
attachedBlock: blockValidIds[1],
|
||||
attachedBlock: blockValidIds[2],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update blocks if the category already matches', async () => {
|
||||
jest.spyOn(blockRepository, 'findOne').mockResolvedValue({
|
||||
id: validIds[0],
|
||||
category: validCategory,
|
||||
nextBlocks: [],
|
||||
attachedBlock: null,
|
||||
} as unknown as Block);
|
||||
|
||||
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
|
||||
jest.spyOn(blockRepository, 'find').mockResolvedValue([]);
|
||||
jest.spyOn(blockRepository, 'updateOne');
|
||||
|
||||
await blockRepository.prepareBlocksInCategoryUpdateScope(
|
||||
validCategory,
|
||||
validIds,
|
||||
category.id,
|
||||
blockValidIds,
|
||||
);
|
||||
|
||||
expect(mockUpdateOne).not.toHaveBeenCalled();
|
||||
expect(blockRepository.find).toHaveBeenCalled();
|
||||
expect(blockRepository.updateOne).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -172,22 +172,24 @@ export class BlockRepository extends BaseRepository<
|
||||
category: string,
|
||||
ids: string[],
|
||||
): Promise<void> {
|
||||
for (const id of ids) {
|
||||
const oldState = await this.findOne(id);
|
||||
if (oldState && oldState.category !== category) {
|
||||
const updatedNextBlocks = oldState.nextBlocks?.filter((nextBlock) =>
|
||||
ids.includes(nextBlock),
|
||||
);
|
||||
const blocks = await this.find({
|
||||
_id: { $in: ids },
|
||||
category: { $ne: category },
|
||||
});
|
||||
|
||||
const updatedAttachedBlock = ids.includes(oldState.attachedBlock || '')
|
||||
? oldState.attachedBlock
|
||||
: null;
|
||||
for (const { id, nextBlocks, attachedBlock } of blocks) {
|
||||
const updatedNextBlocks = nextBlocks.filter((nextBlock) =>
|
||||
ids.includes(nextBlock),
|
||||
);
|
||||
|
||||
await this.updateOne(id, {
|
||||
nextBlocks: updatedNextBlocks,
|
||||
attachedBlock: updatedAttachedBlock,
|
||||
});
|
||||
}
|
||||
const updatedAttachedBlock = ids.includes(attachedBlock || '')
|
||||
? attachedBlock
|
||||
: null;
|
||||
|
||||
await this.updateOne(id, {
|
||||
nextBlocks: updatedNextBlocks,
|
||||
attachedBlock: updatedAttachedBlock,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
this.eventEmitter.emit(
|
||||
'hook:stats:entry',
|
||||
'new_users',
|
||||
BotStatsType.new_users,
|
||||
'New users',
|
||||
created,
|
||||
);
|
||||
|
||||
@@ -42,4 +42,5 @@ export enum PayloadType {
|
||||
button = 'button',
|
||||
outcome = 'outcome',
|
||||
menu = 'menu',
|
||||
content = 'content',
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<any, any>, 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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { BlockService } from '@/chat/services/block.service';
|
||||
import { FieldType } from '@/setting/schemas/types';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import { installContentFixtures } from '@/utils/test/fixtures/content';
|
||||
@@ -100,27 +101,27 @@ describe('ContentTypeController', () => {
|
||||
{
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
label: 'Image',
|
||||
type: 'file',
|
||||
type: FieldType.file,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: 'html',
|
||||
type: FieldType.html,
|
||||
},
|
||||
{
|
||||
name: 'rooms',
|
||||
label: 'Rooms',
|
||||
type: 'file',
|
||||
type: FieldType.file,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
label: 'Price',
|
||||
type: 'file',
|
||||
type: FieldType.file,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
@@ -19,11 +19,12 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { FieldType } from '@/setting/schemas/types';
|
||||
import { DtoConfig } from '@/utils/types/dto.types';
|
||||
|
||||
import { ValidateRequiredFields } from '../validators/validate-required-fields.validator';
|
||||
|
||||
export class FieldType {
|
||||
export class ContentField {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Matches(/^[a-z][a-z_0-9]*$/)
|
||||
@@ -34,11 +35,12 @@ export class FieldType {
|
||||
label: string;
|
||||
|
||||
@IsString()
|
||||
@IsEnum(['text', 'url', 'textarea', 'checkbox', 'file', 'html'], {
|
||||
@IsNotEmpty()
|
||||
@IsEnum(FieldType, {
|
||||
message:
|
||||
"type must be one of the following values: 'text', 'url', 'textarea', 'checkbox', 'file', 'html'",
|
||||
})
|
||||
type: string;
|
||||
type: FieldType;
|
||||
}
|
||||
|
||||
export class ContentTypeCreateDto {
|
||||
@@ -47,13 +49,16 @@ export class ContentTypeCreateDto {
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Content type fields', type: FieldType })
|
||||
@ApiPropertyOptional({
|
||||
description: 'Content type fields',
|
||||
type: ContentField,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Validate(ValidateRequiredFields)
|
||||
@Type(() => FieldType)
|
||||
fields?: FieldType[];
|
||||
@Type(() => ContentField)
|
||||
fields?: ContentField[];
|
||||
}
|
||||
|
||||
export class ContentTypeUpdateDto extends PartialType(ContentTypeCreateDto) {}
|
||||
|
||||
@@ -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,9 +9,12 @@
|
||||
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import { FieldType } from '@/setting/schemas/types';
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
import { ContentField } from '../dto/contentType.dto';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class ContentType extends BaseSchema {
|
||||
/**
|
||||
@@ -30,20 +33,16 @@ export class ContentType extends BaseSchema {
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'checkbox',
|
||||
type: FieldType.checkbox,
|
||||
},
|
||||
],
|
||||
})
|
||||
fields: {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
}[];
|
||||
fields: ContentField[];
|
||||
}
|
||||
|
||||
export const ContentTypeModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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.
|
||||
@@ -12,24 +12,26 @@ import {
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
import { FieldType } from '../dto/contentType.dto';
|
||||
import { FieldType } from '@/setting/schemas/types';
|
||||
|
||||
import { ContentField } from '../dto/contentType.dto';
|
||||
|
||||
@ValidatorConstraint({ name: 'validateRequiredFields', async: false })
|
||||
export class ValidateRequiredFields implements ValidatorConstraintInterface {
|
||||
private readonly REQUIRED_FIELDS: FieldType[] = [
|
||||
private readonly REQUIRED_FIELDS: ContentField[] = [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'checkbox',
|
||||
type: FieldType.checkbox,
|
||||
},
|
||||
];
|
||||
|
||||
validate(fields: FieldType[]): boolean {
|
||||
validate(fields: ContentField[]): boolean {
|
||||
const errors: string[] = [];
|
||||
|
||||
this.REQUIRED_FIELDS.forEach((requiredField, index) => {
|
||||
|
||||
@@ -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,35 @@ 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)) {
|
||||
const settingType = settingTypeMap.get(key);
|
||||
|
||||
switch (settingType) {
|
||||
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);
|
||||
|
||||
@@ -50,11 +50,11 @@ async function bootstrap() {
|
||||
|
||||
const settingService = app.get<SettingService>(SettingService);
|
||||
app.enableCors({
|
||||
origin: (origin, callback) => {
|
||||
settingService
|
||||
origin: async (origin, callback) => {
|
||||
await settingService
|
||||
.getAllowedOrigins()
|
||||
.then((allowedOrigins) => {
|
||||
if (!origin || allowedOrigins.has(origin)) {
|
||||
if (!origin || allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
|
||||
@@ -10,17 +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';
|
||||
@@ -29,11 +26,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 +73,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<NlpValue>({
|
||||
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<NlpValueFull>[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpValueFixturesWithEntities);
|
||||
});
|
||||
|
||||
it('should find nlp Values', async () => {
|
||||
const pageQuery = getPageQuery<NlpValue>({
|
||||
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<NlpValueCreateDto>[],
|
||||
);
|
||||
expect(result).toEqualPayload(nlpValueFixturesWithEntities);
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
it('should count the nlp Values', async () => {
|
||||
const result = await nlpValueController.filterCount();
|
||||
|
||||
@@ -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 {
|
||||
@@ -126,7 +127,7 @@ export class NlpValueController extends BaseController<
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -134,10 +135,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()
|
||||
async findPage(
|
||||
async findWithCount(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<NlpValue>,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
@Query(
|
||||
@@ -147,9 +148,11 @@ export class NlpValueController extends BaseController<
|
||||
)
|
||||
filters: TFilterQuery<NlpValue>,
|
||||
) {
|
||||
return this.canPopulate(populate)
|
||||
? await this.nlpValueService.findAndPopulate(filters, pageQuery)
|
||||
: await this.nlpValueService.find(filters, pageQuery);
|
||||
return await this.nlpValueService.findWithCount(
|
||||
this.canPopulate(populate) ? Format.FULL : Format.STUB,
|
||||
pageQuery,
|
||||
filters,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,10 +8,20 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query } from 'mongoose';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
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';
|
||||
import { TFilterQuery } from '@/utils/types/filter.types';
|
||||
import { Format } from '@/utils/types/format.types';
|
||||
|
||||
import { NlpValueDto } from '../dto/nlp-value.dto';
|
||||
import {
|
||||
@@ -19,7 +29,10 @@ import {
|
||||
NlpValue,
|
||||
NlpValueDocument,
|
||||
NlpValueFull,
|
||||
NlpValueFullWithCount,
|
||||
NlpValuePopulate,
|
||||
NlpValueWithCount,
|
||||
TNlpValueCount,
|
||||
} from '../schemas/nlp-value.schema';
|
||||
|
||||
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
|
||||
@@ -106,4 +119,139 @@ export class NlpValueRepository extends BaseRepository<
|
||||
throw new Error('Attempted to delete a NLP value using unknown criteria');
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @param format - The format can be full or stub
|
||||
* @param pageQuery - The pagination parameters
|
||||
* @param filterQuery - The filter criteria
|
||||
* @returns Aggregated Nlp Value results with sample counts
|
||||
*/
|
||||
private async aggregateWithCount<F extends Format>(
|
||||
format: F,
|
||||
{
|
||||
limit = 10,
|
||||
skip = 0,
|
||||
sort = ['createdAt', 'desc'],
|
||||
}: PageQueryDto<NlpValue>,
|
||||
{ $and = [], ...rest }: TFilterQuery<NlpValue>,
|
||||
): Promise<TNlpValueCount<F>[]> {
|
||||
const pipeline: PipelineStage[] = [
|
||||
{
|
||||
$match: {
|
||||
...rest,
|
||||
...($and.length
|
||||
? {
|
||||
$and: $and.map(({ entity, ...rest }) => ({
|
||||
...rest,
|
||||
...(entity
|
||||
? { entity: new Types.ObjectId(String(entity)) }
|
||||
: {}),
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
{
|
||||
$skip: skip,
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'nlpsampleentities',
|
||||
localField: '_id',
|
||||
foreignField: 'value',
|
||||
as: '_sampleEntities',
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: {
|
||||
path: '$_sampleEntities',
|
||||
preserveNullAndEmptyArrays: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$_id',
|
||||
_originalDoc: {
|
||||
$first: {
|
||||
$unsetField: { input: '$$ROOT', field: 'nlpSamplesCount' },
|
||||
},
|
||||
},
|
||||
nlpSamplesCount: {
|
||||
$sum: { $cond: [{ $ifNull: ['$_sampleEntities', false] }, 1, 0] },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$replaceWith: {
|
||||
$mergeObjects: [
|
||||
'$_originalDoc',
|
||||
{ nlpSamplesCount: '$nlpSamplesCount' },
|
||||
],
|
||||
},
|
||||
},
|
||||
...(format === Format.FULL
|
||||
? [
|
||||
{
|
||||
$lookup: {
|
||||
from: 'nlpentities',
|
||||
localField: 'entity',
|
||||
foreignField: '_id',
|
||||
as: 'entity',
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: '$entity',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
$sort: {
|
||||
[sort[0]]: this.getSortDirection(sort[1]),
|
||||
_id: this.getSortDirection(sort[1]),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return await this.model.aggregate<TNlpValueCount<F>>(pipeline).exec();
|
||||
}
|
||||
|
||||
async findWithCount<F extends Format>(
|
||||
format: F,
|
||||
pageQuery: PageQueryDto<NlpValue>,
|
||||
filterQuery: TFilterQuery<NlpValue>,
|
||||
): Promise<TNlpValueCount<F>[]> {
|
||||
try {
|
||||
const aggregatedResults = await this.aggregateWithCount(
|
||||
format,
|
||||
pageQuery,
|
||||
filterQuery,
|
||||
);
|
||||
|
||||
if (format === Format.FULL) {
|
||||
return plainToInstance(NlpValueFullWithCount, aggregatedResults, {
|
||||
excludePrefixes: ['_'],
|
||||
}) as TNlpValueCount<F>[];
|
||||
}
|
||||
|
||||
return plainToInstance(NlpValueWithCount, aggregatedResults, {
|
||||
excludePrefixes: ['_'],
|
||||
}) as TNlpValueCount<F>[];
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in findWithCount: ${error.message}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -106,6 +107,14 @@ export class NlpValueFull extends NlpValueStub {
|
||||
entity: NlpEntity;
|
||||
}
|
||||
|
||||
export class NlpValueWithCount extends NlpValue {
|
||||
nlpSamplesCount: number;
|
||||
}
|
||||
|
||||
export class NlpValueFullWithCount extends NlpValueFull {
|
||||
nlpSamplesCount: number;
|
||||
}
|
||||
|
||||
export type NlpValueDocument = THydratedDocument<NlpValue>;
|
||||
|
||||
export const NlpValueModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
@@ -121,3 +130,9 @@ export type NlpValuePopulate = keyof TFilterPopulateFields<
|
||||
>;
|
||||
|
||||
export const NLP_VALUE_POPULATE: NlpValuePopulate[] = ['entity'];
|
||||
|
||||
export type TNlpValueCount<T> = TStubOrFull<
|
||||
T,
|
||||
NlpValueWithCount,
|
||||
NlpValueFullWithCount
|
||||
>;
|
||||
|
||||
@@ -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.
|
||||
@@ -10,6 +10,9 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
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';
|
||||
@@ -18,6 +21,7 @@ import {
|
||||
NlpValue,
|
||||
NlpValueFull,
|
||||
NlpValuePopulate,
|
||||
TNlpValueCount,
|
||||
} from '../schemas/nlp-value.schema';
|
||||
import { NlpSampleEntityValue } from '../schemas/types';
|
||||
|
||||
@@ -218,4 +222,12 @@ export class NlpValueService extends BaseService<
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
async findWithCount<F extends Format>(
|
||||
format: F,
|
||||
pageQuery: PageQueryDto<NlpValue>,
|
||||
filters: TFilterQuery<NlpValue>,
|
||||
): Promise<TNlpValueCount<F>[]> {
|
||||
return await this.repository.findWithCount(format, pageQuery, filters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -20,6 +20,15 @@ export enum SettingType {
|
||||
multiple_attachment = 'multiple_attachment',
|
||||
}
|
||||
|
||||
export enum FieldType {
|
||||
text = 'text',
|
||||
url = 'url',
|
||||
textarea = 'textarea',
|
||||
checkbox = 'checkbox',
|
||||
file = 'file',
|
||||
html = 'html',
|
||||
}
|
||||
|
||||
/**
|
||||
* The following interfaces are declared, and currently not used
|
||||
* TextSetting
|
||||
|
||||
@@ -161,14 +161,12 @@ describe('SettingService', () => {
|
||||
expect(settingService.find).toHaveBeenCalledWith({
|
||||
label: 'allowed_domains',
|
||||
});
|
||||
expect(result).toEqual(
|
||||
new Set([
|
||||
'*',
|
||||
'https://example.com',
|
||||
'https://test.com',
|
||||
'https://another.com',
|
||||
]),
|
||||
);
|
||||
expect(result).toEqual([
|
||||
'*',
|
||||
'https://example.com',
|
||||
'https://test.com',
|
||||
'https://another.com',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the config allowed cors only if no settings are found', async () => {
|
||||
@@ -179,7 +177,7 @@ describe('SettingService', () => {
|
||||
expect(settingService.find).toHaveBeenCalledWith({
|
||||
label: 'allowed_domains',
|
||||
});
|
||||
expect(result).toEqual(new Set(['*']));
|
||||
expect(result).toEqual(['*']);
|
||||
});
|
||||
|
||||
it('should handle settings with empty values', async () => {
|
||||
@@ -195,7 +193,7 @@ describe('SettingService', () => {
|
||||
expect(settingService.find).toHaveBeenCalledWith({
|
||||
label: 'allowed_domains',
|
||||
});
|
||||
expect(result).toEqual(new Set(['*', 'https://example.com']));
|
||||
expect(result).toEqual(['*', 'https://example.com']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ export class SettingService extends BaseService<Setting> {
|
||||
* @returns A promise that resolves to a set of allowed origins
|
||||
*/
|
||||
@Cacheable(ALLOWED_ORIGINS_CACHE_KEY)
|
||||
async getAllowedOrigins() {
|
||||
async getAllowedOrigins(): Promise<string[]> {
|
||||
const settings = (await this.find({
|
||||
label: 'allowed_domains',
|
||||
})) as TextSetting[];
|
||||
@@ -150,7 +150,7 @@ export class SettingService extends BaseService<Setting> {
|
||||
...allowedDomains,
|
||||
]);
|
||||
|
||||
return uniqueOrigins;
|
||||
return Array.from(uniqueOrigins);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
31
api/src/utils/test/fixtures/contenttype.ts
vendored
31
api/src/utils/test/fixtures/contenttype.ts
vendored
@@ -13,6 +13,7 @@ import {
|
||||
ContentType,
|
||||
ContentTypeModel,
|
||||
} from '@/cms/schemas/content-type.schema';
|
||||
import { FieldType } from '@/setting/schemas/types';
|
||||
|
||||
import { getFixturesWithDefaultValues } from '../defaultValues';
|
||||
import { FixturesTypeBuilder } from '../types';
|
||||
@@ -27,12 +28,12 @@ export const contentTypeDefaultValues: TContentTypeFixtures['defaultValues'] = {
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'checkbox',
|
||||
type: FieldType.checkbox,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -44,27 +45,27 @@ const contentTypes: TContentTypeFixtures['values'][] = [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'checkbox',
|
||||
type: FieldType.checkbox,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
label: 'Image',
|
||||
type: 'file',
|
||||
type: FieldType.file,
|
||||
},
|
||||
{
|
||||
name: 'subtitle',
|
||||
label: 'Image',
|
||||
type: 'file',
|
||||
type: FieldType.file,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -74,22 +75,22 @@ const contentTypes: TContentTypeFixtures['values'][] = [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'checkbox',
|
||||
type: FieldType.checkbox,
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
label: 'Image',
|
||||
type: 'file',
|
||||
type: FieldType.file,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -99,22 +100,22 @@ const contentTypes: TContentTypeFixtures['values'][] = [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'checkbox',
|
||||
type: FieldType.checkbox,
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
type: 'text',
|
||||
type: FieldType.text,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
label: 'Image',
|
||||
type: 'file',
|
||||
type: FieldType.file,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
18
api/src/utils/types/format.types.ts
Normal file
18
api/src/utils/types/format.types.ts
Normal file
@@ -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, TStub, TFull> = TF extends Format.STUB
|
||||
? TStub
|
||||
: TFull;
|
||||
@@ -54,15 +54,15 @@ export const buildWebSocketGatewayOptions = (): Partial<ServerOptions> => {
|
||||
...(config.sockets.cookie && { cookie: config.sockets.cookie }),
|
||||
...(config.sockets.onlyAllowOrigins && {
|
||||
cors: {
|
||||
origin: (origin, cb) => {
|
||||
origin: async (origin, cb) => {
|
||||
// Retrieve the allowed origins from the settings
|
||||
const app = AppInstance.getApp();
|
||||
const settingService = app.get<SettingService>(SettingService);
|
||||
|
||||
settingService
|
||||
await settingService
|
||||
.getAllowedOrigins()
|
||||
.then((allowedOrigins) => {
|
||||
if (origin && allowedOrigins.has(origin)) {
|
||||
if (origin && allowedOrigins.includes(origin)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -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,9 +43,11 @@
|
||||
"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/qs": "^6.9.15",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/random-seed": "^0.3.5",
|
||||
"@types/react": "18.3.2",
|
||||
@@ -57,7 +58,6 @@
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -334,6 +334,7 @@
|
||||
"nlp": "NLU",
|
||||
"nlp_entity": "Entity",
|
||||
"nlp_entity_value": "Value",
|
||||
"nlp_samples_count": "Samples count",
|
||||
"value": "Value",
|
||||
"synonyms": "Synonyms",
|
||||
"lookups": "Lookups",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
@@ -53,7 +54,7 @@ export const SubscribersList = (props: {
|
||||
<Grid padding={2}>
|
||||
<Title title={t(props.assignedTo)} icon={InboxIcon} />
|
||||
</Grid>
|
||||
{subscribers?.length > 0 && (
|
||||
{subscribers?.length > 0 ? (
|
||||
<ConversationList
|
||||
scrollable
|
||||
loading={isFetching}
|
||||
@@ -64,7 +65,10 @@ export const SubscribersList = (props: {
|
||||
<Conversation
|
||||
onClick={() => {
|
||||
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}
|
||||
@@ -87,6 +91,10 @@ export const SubscribersList = (props: {
|
||||
</Conversation>
|
||||
))}
|
||||
</ConversationList>
|
||||
) : (
|
||||
<Grid p={1} color="gray" textAlign="center">
|
||||
{t("message.no_result_found")}
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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<ISubscriber>({
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>({
|
||||
$or: ["first_name", "last_name"],
|
||||
});
|
||||
const [channels, setChannels] = useState<string[]>([]);
|
||||
@@ -48,6 +48,7 @@ export const Inbox = () => {
|
||||
<Sidebar position="left">
|
||||
<Grid paddingX={1} paddingTop={1}>
|
||||
<Search
|
||||
value={searchText}
|
||||
onClearClick={() => onSearch("")}
|
||||
className="changeColor"
|
||||
onChange={(v) => onSearch(v)}
|
||||
|
||||
@@ -392,6 +392,7 @@ export default function NlpSample() {
|
||||
`nlpsample/export${type ? `?type=${type}` : ""}`,
|
||||
)}
|
||||
startIcon={<DownloadIcon />}
|
||||
disabled={dataGridProps?.rows?.length === 0}
|
||||
>
|
||||
{t("button.export")}
|
||||
</Button>
|
||||
|
||||
@@ -34,7 +34,6 @@ import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { PageHeader } from "@/layout/content/PageHeader";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { NlpLookups } from "@/types/nlp-entity.types";
|
||||
import { INlpValue } from "@/types/nlp-value.types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
import { getDateTimeFormatter } from "@/utils/date";
|
||||
@@ -52,13 +51,12 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
entity: EntityType.NLP_ENTITY,
|
||||
format: Format.FULL,
|
||||
});
|
||||
const canHaveSynonyms = nlpEntity?.lookups?.[0] === NlpLookups.keywords;
|
||||
const { onSearch, searchPayload } = useSearch<INlpValue>({
|
||||
$eq: [{ entity: entityId }],
|
||||
$or: ["doc", "value"]
|
||||
$or: ["doc", "value"],
|
||||
});
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.NLP_VALUE },
|
||||
{ entity: EntityType.NLP_VALUE, format: Format.FULL },
|
||||
{
|
||||
params: searchPayload,
|
||||
},
|
||||
@@ -88,7 +86,10 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
{
|
||||
label: ActionColumnLabel.Edit,
|
||||
action: (row) =>
|
||||
dialogs.open(NlpValueFormDialog, { data: row, canHaveSynonyms }),
|
||||
dialogs.open(NlpValueFormDialog, {
|
||||
defaultValues: row,
|
||||
presetValues: nlpEntity,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: ActionColumnLabel.Delete,
|
||||
@@ -103,7 +104,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
],
|
||||
t("label.operations"),
|
||||
);
|
||||
const synonymsColumn = {
|
||||
const synonymsColumn = {
|
||||
flex: 3,
|
||||
field: "synonyms",
|
||||
headerName: t("label.synonyms"),
|
||||
@@ -125,6 +126,24 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
},
|
||||
{
|
||||
flex: 2,
|
||||
field: "nlpSamplesCount",
|
||||
align: "center",
|
||||
headerName: t("label.nlp_samples_count"),
|
||||
sortable: true,
|
||||
disableColumnMenu: true,
|
||||
headerAlign: "center",
|
||||
renderHeader,
|
||||
renderCell: ({ row }) => (
|
||||
<Chip
|
||||
sx={{ alignContent: "center" }}
|
||||
id={row.id}
|
||||
label={row.nlpSamplesCount}
|
||||
variant="inbox"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
flex: 3,
|
||||
field: "doc",
|
||||
@@ -220,7 +239,11 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
startIcon={<AddIcon />}
|
||||
variant="contained"
|
||||
sx={{ float: "right" }}
|
||||
onClick={() => dialogs.open(NlpValueFormDialog, null)}
|
||||
onClick={() =>
|
||||
dialogs.open(NlpValueFormDialog, {
|
||||
presetValues: nlpEntity,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("button.add")}
|
||||
</Button>
|
||||
|
||||
@@ -20,19 +20,31 @@ import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { ComponentFormProps } from "@/types/common/dialogs.types";
|
||||
import { INlpEntity, NlpLookups } from "@/types/nlp-entity.types";
|
||||
import { INlpValue, INlpValueAttributes } from "@/types/nlp-value.types";
|
||||
|
||||
export const NlpValueForm: FC<
|
||||
ComponentFormProps<{ data: INlpValue; canHaveSynonyms: boolean }>
|
||||
> = ({ data: props, Wrapper = Fragment, WrapperProps, ...rest }) => {
|
||||
const { data, canHaveSynonyms } = props || {};
|
||||
export type NlpValueFormProps = {
|
||||
defaultValues?: INlpValue;
|
||||
presetValues: INlpEntity | undefined;
|
||||
};
|
||||
export const NlpValueForm: FC<ComponentFormProps<NlpValueFormProps>> = ({
|
||||
data: props,
|
||||
Wrapper = Fragment,
|
||||
WrapperProps,
|
||||
...rest
|
||||
}) => {
|
||||
const { defaultValues: nlpValue, presetValues: nlpEntity } = props || {
|
||||
defaultValues: null,
|
||||
presetValues: null,
|
||||
};
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { query } = useRouter();
|
||||
const { refetch: refetchEntity } = useGet(data?.entity || String(query.id), {
|
||||
const { refetch: refetchEntity } = useGet(nlpEntity?.id!, {
|
||||
entity: EntityType.NLP_ENTITY,
|
||||
format: Format.FULL,
|
||||
});
|
||||
const canHaveSynonyms = nlpEntity?.lookups.includes(NlpLookups.keywords);
|
||||
const { mutate: createNlpValue } = useCreate(EntityType.NLP_VALUE, {
|
||||
onError: () => {
|
||||
rest.onError?.();
|
||||
@@ -54,15 +66,21 @@ export const NlpValueForm: FC<
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const { reset, register, handleSubmit, control } = useForm<
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<
|
||||
INlpValueAttributes & {
|
||||
expressions: string[];
|
||||
}
|
||||
>({
|
||||
defaultValues: {
|
||||
value: data?.value || "",
|
||||
doc: data?.doc || "",
|
||||
expressions: data?.expressions || [],
|
||||
value: nlpValue?.value || "",
|
||||
doc: nlpValue?.doc || "",
|
||||
expressions: nlpValue?.expressions || [],
|
||||
},
|
||||
});
|
||||
const validationRules = {
|
||||
@@ -73,24 +91,24 @@ export const NlpValueForm: FC<
|
||||
description: {},
|
||||
};
|
||||
const onSubmitForm = async (params: INlpValueAttributes) => {
|
||||
if (data) {
|
||||
updateNlpValue({ id: data.id, params });
|
||||
if (nlpValue) {
|
||||
updateNlpValue({ id: nlpValue.id, params });
|
||||
} else {
|
||||
createNlpValue({ ...params, entity: String(query.id) });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (nlpValue) {
|
||||
reset({
|
||||
value: data.value,
|
||||
expressions: data.expressions,
|
||||
doc: data.doc,
|
||||
value: nlpValue.value,
|
||||
expressions: nlpValue.expressions,
|
||||
doc: nlpValue.doc,
|
||||
});
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
}, [data, reset]);
|
||||
}, [nlpValue, reset]);
|
||||
|
||||
return (
|
||||
<Wrapper onSubmit={handleSubmit(onSubmitForm)} {...WrapperProps}>
|
||||
@@ -99,8 +117,10 @@ export const NlpValueForm: FC<
|
||||
<ContentItem>
|
||||
<Input
|
||||
label={t("placeholder.nlp_value")}
|
||||
error={!!errors.value}
|
||||
required
|
||||
autoFocus
|
||||
helperText={errors.value ? errors.value.message : null}
|
||||
{...register("value", validationRules.value)}
|
||||
/>
|
||||
</ContentItem>
|
||||
@@ -118,7 +138,11 @@ export const NlpValueForm: FC<
|
||||
name="expressions"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<MultipleInput label="synonyms" {...field} />
|
||||
<MultipleInput
|
||||
label={t("label.synonyms")}
|
||||
{...field}
|
||||
minInput={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ContentItem>
|
||||
|
||||
@@ -8,20 +8,17 @@
|
||||
|
||||
import { GenericFormDialog } from "@/app-components/dialogs";
|
||||
import { ComponentFormDialogProps } from "@/types/common/dialogs.types";
|
||||
import { INlpValue } from "@/types/nlp-value.types";
|
||||
|
||||
import { NlpValueForm } from "./NlpValueForm";
|
||||
import { NlpValueForm, NlpValueFormProps } from "./NlpValueForm";
|
||||
|
||||
export const NlpValueFormDialog = <
|
||||
T extends { data: INlpValue; canHaveSynonyms: boolean } = {
|
||||
data: INlpValue;
|
||||
canHaveSynonyms: boolean;
|
||||
},
|
||||
T extends NlpValueFormProps = NlpValueFormProps,
|
||||
>(
|
||||
props: ComponentFormDialogProps<T>,
|
||||
) => (
|
||||
<GenericFormDialog<T>
|
||||
Form={NlpValueForm}
|
||||
rowKey="defaultValues"
|
||||
addText="title.new_nlp_entity_value"
|
||||
editText="title.edit_nlp_value"
|
||||
{...props}
|
||||
|
||||
@@ -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({
|
||||
@@ -23,7 +31,7 @@ function BlockFormProvider({
|
||||
methods,
|
||||
block,
|
||||
}: {
|
||||
methods: UseFormReturn<IBlockAttributes, any, undefined>;
|
||||
methods: UseFormReturn<IBlockAttributes>;
|
||||
block: IBlock | undefined;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
|
||||
@@ -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 <Progress />;
|
||||
if (isLoading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
|
||||
@@ -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 = <T,>({ params, searchText }: TBuildParamProps<T>) => ({
|
||||
@@ -52,13 +53,38 @@ const buildNeqInitialParams = <T,>({
|
||||
);
|
||||
|
||||
export const useSearch = <T,>(params: TParamItem<T>) => {
|
||||
const [searchText, setSearchText] = useState<string>("");
|
||||
const onSearch = debounce(
|
||||
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string) => {
|
||||
setSearchText(typeof e === "string" ? e : e.target.value);
|
||||
},
|
||||
300,
|
||||
const router = useRouter();
|
||||
const [searchText, setSearchText] = useState<string>(
|
||||
(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<HTMLInputElement | HTMLTextAreaElement> | 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 = <T,>(params: TParamItem<T>) => {
|
||||
} = params;
|
||||
|
||||
return {
|
||||
searchText,
|
||||
onSearch,
|
||||
searchPayload: {
|
||||
where: {
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface INlpValueAttributes {
|
||||
expressions?: string[];
|
||||
metadata?: Record<string, any>;
|
||||
builtin?: boolean;
|
||||
nlpSamplesCount?: number;
|
||||
}
|
||||
|
||||
export interface INlpValueStub extends IBaseSchema, INlpValueAttributes {}
|
||||
|
||||
@@ -6,25 +6,7 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
export const getFromQuery = ({
|
||||
key,
|
||||
search,
|
||||
defaultValue = "",
|
||||
}: {
|
||||
key: string;
|
||||
search?: string;
|
||||
defaultValue?: string;
|
||||
}) => {
|
||||
try {
|
||||
const paramsString = search || window.location.search;
|
||||
const searchParams = new URLSearchParams(paramsString);
|
||||
const loadCampaign = searchParams.get(key) || defaultValue;
|
||||
|
||||
return loadCampaign;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
import qs from "qs";
|
||||
|
||||
export const buildURL = (baseUrl: string, relativePath: string): string => {
|
||||
try {
|
||||
@@ -57,3 +39,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);
|
||||
};
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -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",
|
||||
@@ -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": {
|
||||
@@ -2877,6 +2878,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": {
|
||||
@@ -10307,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",
|
||||
@@ -10317,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user