mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge branch 'main' into feat/annotate-sample-with-keyword-entities
This commit is contained in:
@@ -89,5 +89,6 @@ module.exports = {
|
||||
],
|
||||
2,
|
||||
],
|
||||
'no-multiple-empty-lines': ['error', { max: 1 }],
|
||||
},
|
||||
};
|
||||
|
||||
4
api/package-lock.json
generated
4
api/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hexabot",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hexabot",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hexabot",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.5",
|
||||
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
|
||||
"author": "Hexastack",
|
||||
"license": "AGPL-3.0-only",
|
||||
|
||||
@@ -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.
|
||||
@@ -25,9 +25,11 @@ import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { InvitationRepository } from '@/user/repositories/invitation.repository';
|
||||
import { PermissionRepository } from '@/user/repositories/permission.repository';
|
||||
import { RoleRepository } from '@/user/repositories/role.repository';
|
||||
import { UserRepository } from '@/user/repositories/user.repository';
|
||||
import { InvitationModel } from '@/user/schemas/invitation.schema';
|
||||
import { PermissionModel } from '@/user/schemas/permission.schema';
|
||||
import { RoleModel } from '@/user/schemas/role.schema';
|
||||
import { UserModel } from '@/user/schemas/user.schema';
|
||||
@@ -86,6 +88,7 @@ describe('BlockController', () => {
|
||||
LabelModel,
|
||||
CategoryModel,
|
||||
ContentModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
UserModel,
|
||||
RoleModel,
|
||||
@@ -102,6 +105,7 @@ describe('BlockController', () => {
|
||||
UserRepository,
|
||||
RoleRepository,
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
LabelService,
|
||||
@@ -251,6 +255,7 @@ describe('BlockController', () => {
|
||||
name: 'block with nextBlocks',
|
||||
nextBlocks: [hasNextBlocks.id],
|
||||
patterns: ['Hi'],
|
||||
outcomes: [],
|
||||
trigger_labels: [],
|
||||
assign_labels: [],
|
||||
trigger_channels: [],
|
||||
|
||||
@@ -6,7 +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).
|
||||
*/
|
||||
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
@@ -15,13 +15,16 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { InvitationRepository } from '@/user/repositories/invitation.repository';
|
||||
import { RoleRepository } from '@/user/repositories/role.repository';
|
||||
import { UserRepository } from '@/user/repositories/user.repository';
|
||||
import { InvitationModel } from '@/user/schemas/invitation.schema';
|
||||
import { PermissionModel } from '@/user/schemas/permission.schema';
|
||||
import { RoleModel } from '@/user/schemas/role.schema';
|
||||
import { UserModel } from '@/user/schemas/user.schema';
|
||||
import { RoleService } from '@/user/services/role.service';
|
||||
import { UserService } from '@/user/services/user.service';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import { labelFixtures } from '@/utils/test/fixtures/label';
|
||||
@@ -48,6 +51,7 @@ describe('LabelController', () => {
|
||||
let labelService: LabelService;
|
||||
let label: Label;
|
||||
let labelToDelete: Label;
|
||||
let secondLabelToDelete: Label;
|
||||
let subscriberService: SubscriberService;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -61,6 +65,7 @@ describe('LabelController', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
SubscriberModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
],
|
||||
@@ -73,6 +78,7 @@ describe('LabelController', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
SubscriberService,
|
||||
SubscriberRepository,
|
||||
EventEmitter2,
|
||||
@@ -87,6 +93,9 @@ describe('LabelController', () => {
|
||||
labelToDelete = (await labelService.findOne({
|
||||
name: 'TEST_TITLE_2',
|
||||
})) as Label;
|
||||
secondLabelToDelete = (await labelService.findOne({
|
||||
name: 'TEST_TITLE_3',
|
||||
})) as Label;
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
@@ -230,4 +239,32 @@ describe('LabelController', () => {
|
||||
).rejects.toThrow(getUpdateOneError(Label.name, labelToDelete.id));
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should delete multiple labels', async () => {
|
||||
const valuesToDelete = [label.id, secondLabelToDelete.id];
|
||||
|
||||
const result = await labelController.deleteMany(valuesToDelete);
|
||||
|
||||
expect(result.deletedCount).toEqual(valuesToDelete.length);
|
||||
const remainingValues = await labelService.find({
|
||||
_id: { $in: valuesToDelete },
|
||||
});
|
||||
expect(remainingValues.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when no IDs are provided', async () => {
|
||||
await expect(labelController.deleteMany([])).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when provided IDs do not exist', async () => {
|
||||
const nonExistentIds = [NOT_FOUND_ID, NOT_FOUND_ID.replace(/9/g, '8')];
|
||||
|
||||
await expect(labelController.deleteMany(nonExistentIds)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
@@ -24,6 +25,7 @@ import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
|
||||
@@ -125,4 +127,29 @@ export class LabelController extends BaseController<
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple Labels by their IDs.
|
||||
* @param ids - IDs of Labels to be deleted.
|
||||
* @returns A Promise that resolves to the deletion result.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete('')
|
||||
@HttpCode(204)
|
||||
async deleteMany(@Body('ids') ids: string[]): Promise<DeleteResult> {
|
||||
if (!ids || ids.length === 0) {
|
||||
throw new BadRequestException('No IDs provided for deletion.');
|
||||
}
|
||||
const deleteResult = await this.labelService.deleteMany({
|
||||
_id: { $in: ids },
|
||||
});
|
||||
|
||||
if (deleteResult.deletedCount === 0) {
|
||||
this.logger.warn(`Unable to delete Labels with provided IDs: ${ids}`);
|
||||
throw new NotFoundException('Labels with provided IDs not found');
|
||||
}
|
||||
|
||||
this.logger.log(`Successfully deleted Labels with IDs: ${ids}`);
|
||||
return deleteResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { InvitationRepository } from '@/user/repositories/invitation.repository';
|
||||
import { RoleRepository } from '@/user/repositories/role.repository';
|
||||
import { UserRepository } from '@/user/repositories/user.repository';
|
||||
import { InvitationModel } from '@/user/schemas/invitation.schema';
|
||||
import { PermissionModel } from '@/user/schemas/permission.schema';
|
||||
import { RoleModel } from '@/user/schemas/role.schema';
|
||||
import { User, UserModel } from '@/user/schemas/user.schema';
|
||||
@@ -71,6 +73,7 @@ describe('MessageController', () => {
|
||||
MessageModel,
|
||||
UserModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
MenuModel,
|
||||
@@ -85,6 +88,7 @@ describe('MessageController', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
SubscriberRepository,
|
||||
ChannelService,
|
||||
AttachmentService,
|
||||
|
||||
@@ -14,8 +14,10 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { InvitationRepository } from '@/user/repositories/invitation.repository';
|
||||
import { RoleRepository } from '@/user/repositories/role.repository';
|
||||
import { UserRepository } from '@/user/repositories/user.repository';
|
||||
import { InvitationModel } from '@/user/schemas/invitation.schema';
|
||||
import { PermissionModel } from '@/user/schemas/permission.schema';
|
||||
import { RoleModel } from '@/user/schemas/role.schema';
|
||||
import { User, UserModel } from '@/user/schemas/user.schema';
|
||||
@@ -63,6 +65,7 @@ describe('SubscriberController', () => {
|
||||
LabelModel,
|
||||
UserModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
@@ -79,6 +82,7 @@ describe('SubscriberController', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
EventEmitter2,
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
|
||||
@@ -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.
|
||||
@@ -45,6 +45,14 @@ export class BlockCreateDto {
|
||||
@IsPatternList({ message: 'Patterns list is invalid' })
|
||||
patterns?: Pattern[] = [];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Block's outcomes",
|
||||
type: Array,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray({ message: 'Outcomes are invalid' })
|
||||
outcomes?: string[] = [];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Block trigger labels', type: Array })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@@ -120,6 +128,7 @@ export class BlockCreateDto {
|
||||
export class BlockUpdateDto extends PartialType(
|
||||
OmitType(BlockCreateDto, [
|
||||
'patterns',
|
||||
'outcomes',
|
||||
'trigger_labels',
|
||||
'assign_labels',
|
||||
'trigger_channels',
|
||||
@@ -130,6 +139,14 @@ export class BlockUpdateDto extends PartialType(
|
||||
@IsPatternList({ message: 'Patterns list is invalid' })
|
||||
patterns?: Pattern[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Block's outcomes",
|
||||
type: Array,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray({ message: 'Outcomes are invalid' })
|
||||
outcomes?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Block trigger labels', type: Array })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
|
||||
@@ -54,15 +54,20 @@ export class CategoryRepository extends BaseRepository<
|
||||
criteria: TFilterQuery<Category>,
|
||||
) {
|
||||
criteria = query.getQuery();
|
||||
const ids = Array.isArray(criteria._id) ? criteria._id : [criteria._id];
|
||||
const ids = Array.isArray(criteria._id?.$in)
|
||||
? criteria._id.$in
|
||||
: Array.isArray(criteria._id)
|
||||
? criteria._id
|
||||
: [criteria._id];
|
||||
|
||||
for (const id of ids) {
|
||||
const associatedBlocks = await this.blockService.findOne({
|
||||
category: id,
|
||||
});
|
||||
if (associatedBlocks) {
|
||||
const category = await this.findOne({ _id: id });
|
||||
throw new ForbiddenException(
|
||||
`Category ${id} has blocks associated with it`,
|
||||
`Category ${category?.label || id} has blocks associated with it`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,13 @@ export class LabelRepository extends BaseRepository<
|
||||
>,
|
||||
_criteria: TFilterQuery<Label>,
|
||||
): Promise<void> {
|
||||
const labels = await this.find(
|
||||
typeof _criteria === 'string' ? { _id: _criteria } : _criteria,
|
||||
);
|
||||
const ids = Array.isArray(_criteria._id?.$in)
|
||||
? _criteria._id.$in
|
||||
: Array.isArray(_criteria._id)
|
||||
? _criteria._id
|
||||
: [_criteria._id];
|
||||
|
||||
const labels = await this.find({ _id: { $in: ids } });
|
||||
this.eventEmitter.emit('hook:label:delete', labels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ export class BlockStub extends BaseSchema {
|
||||
})
|
||||
patterns: Pattern[];
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
default: [],
|
||||
})
|
||||
outcomes: string[];
|
||||
|
||||
@Prop([
|
||||
{
|
||||
type: MongooseSchema.Types.ObjectId,
|
||||
|
||||
@@ -34,3 +34,11 @@ export type PostBackButton = z.infer<typeof postBackButtonSchema>;
|
||||
export type WebUrlButton = z.infer<typeof webUrlButtonSchema>;
|
||||
|
||||
export type Button = z.infer<typeof buttonSchema>;
|
||||
|
||||
export enum PayloadType {
|
||||
location = 'location',
|
||||
attachments = 'attachments',
|
||||
quick_reply = 'quick_reply',
|
||||
button = 'button',
|
||||
outcome = 'outcome',
|
||||
}
|
||||
|
||||
@@ -6,14 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
|
||||
import { PluginName } from '@/plugins/types';
|
||||
@@ -21,7 +13,7 @@ import { PluginName } from '@/plugins/types';
|
||||
import { Message } from '../message.schema';
|
||||
|
||||
import { attachmentPayloadSchema } from './attachment';
|
||||
import { buttonSchema } from './button';
|
||||
import { buttonSchema, PayloadType } from './button';
|
||||
import { contentOptionsSchema } from './options';
|
||||
import { QuickReplyType, stdQuickReplySchema } from './quick-reply';
|
||||
|
||||
@@ -62,6 +54,7 @@ export enum OutgoingMessageFormat {
|
||||
attachment = 'attachment',
|
||||
list = 'list',
|
||||
carousel = 'carousel',
|
||||
system = 'system',
|
||||
}
|
||||
|
||||
export const outgoingMessageFormatSchema = z.nativeEnum(OutgoingMessageFormat);
|
||||
@@ -85,13 +78,6 @@ export const fileTypeSchema = z.nativeEnum(FileType);
|
||||
|
||||
export type FileTypeLiteral = z.infer<typeof fileTypeSchema>;
|
||||
|
||||
export enum PayloadType {
|
||||
location = 'location',
|
||||
attachments = 'attachments',
|
||||
quick_reply = 'quick_reply',
|
||||
button = 'button',
|
||||
}
|
||||
|
||||
export const payloadTypeSchema = z.nativeEnum(PayloadType);
|
||||
|
||||
export type PayloadTypeLiteral = z.infer<typeof payloadTypeSchema>;
|
||||
@@ -154,6 +140,15 @@ export type StdOutgoingAttachmentMessage = z.infer<
|
||||
typeof stdOutgoingAttachmentMessageSchema
|
||||
>;
|
||||
|
||||
export const stdOutgoingSystemMessageSchema = z.object({
|
||||
outcome: z.string().optional(), // "any" or any other string (in snake case)
|
||||
data: z.any().optional(),
|
||||
});
|
||||
|
||||
export type StdOutgoingSystemMessage = z.infer<
|
||||
typeof stdOutgoingSystemMessageSchema
|
||||
>;
|
||||
|
||||
export const pluginNameSchema = z
|
||||
.string()
|
||||
.regex(/-plugin$/) as z.ZodType<PluginName>;
|
||||
@@ -297,7 +292,16 @@ export type StdOutgoingAttachmentEnvelope = z.infer<
|
||||
typeof stdOutgoingAttachmentEnvelopeSchema
|
||||
>;
|
||||
|
||||
export const stdOutgoingEnvelopeSchema = z.union([
|
||||
export const stdOutgoingSystemEnvelopeSchema = z.object({
|
||||
format: z.literal(OutgoingMessageFormat.system),
|
||||
message: stdOutgoingSystemMessageSchema,
|
||||
});
|
||||
|
||||
export type StdOutgoingSystemEnvelope = z.infer<
|
||||
typeof stdOutgoingSystemEnvelopeSchema
|
||||
>;
|
||||
|
||||
export const stdOutgoingMessageEnvelopeSchema = z.union([
|
||||
stdOutgoingTextEnvelopeSchema,
|
||||
stdOutgoingQuickRepliesEnvelopeSchema,
|
||||
stdOutgoingButtonsEnvelopeSchema,
|
||||
@@ -305,6 +309,15 @@ export const stdOutgoingEnvelopeSchema = z.union([
|
||||
stdOutgoingAttachmentEnvelopeSchema,
|
||||
]);
|
||||
|
||||
export type StdOutgoingMessageEnvelope = z.infer<
|
||||
typeof stdOutgoingMessageEnvelopeSchema
|
||||
>;
|
||||
|
||||
export const stdOutgoingEnvelopeSchema = z.union([
|
||||
stdOutgoingMessageEnvelopeSchema,
|
||||
stdOutgoingSystemEnvelopeSchema,
|
||||
]);
|
||||
|
||||
export type StdOutgoingEnvelope = z.infer<typeof stdOutgoingEnvelopeSchema>;
|
||||
|
||||
// is-valid-message-text validation
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PayloadType } from './message';
|
||||
import { PayloadType } from './button';
|
||||
|
||||
export const payloadPatternSchema = z.object({
|
||||
label: z.string(),
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { attachmentPayloadSchema } from './attachment';
|
||||
import { PayloadType } from './button';
|
||||
|
||||
export enum QuickReplyType {
|
||||
text = 'text',
|
||||
@@ -24,11 +25,11 @@ export const cordinatesSchema = z.object({
|
||||
|
||||
export const payloadSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('location'),
|
||||
type: z.literal(PayloadType.location),
|
||||
coordinates: cordinatesSchema,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('attachments'),
|
||||
type: z.literal(PayloadType.attachments),
|
||||
attachment: attachmentPayloadSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
subscriberWithLabels,
|
||||
subscriberWithoutLabels,
|
||||
} from '@/channel/lib/__test__/subscriber.mock';
|
||||
import { PayloadType } from '@/chat/schemas/types/button';
|
||||
import { ContentTypeRepository } from '@/cms/repositories/content-type.repository';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { ContentTypeModel } from '@/cms/schemas/content-type.schema';
|
||||
@@ -64,7 +65,7 @@ import { Category, CategoryModel } from '../schemas/category.schema';
|
||||
import { LabelModel } from '../schemas/label.schema';
|
||||
import { FileType } from '../schemas/types/attachment';
|
||||
import { Context } from '../schemas/types/context';
|
||||
import { PayloadType, StdOutgoingListMessage } from '../schemas/types/message';
|
||||
import { StdOutgoingListMessage } from '../schemas/types/message';
|
||||
import { SubscriberContext } from '../schemas/types/subscriberContext';
|
||||
|
||||
import { CategoryRepository } from './../repositories/category.repository';
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { CONSOLE_CHANNEL_NAME } from '@/extensions/channels/console/settings';
|
||||
import { NLU } from '@/helper/types';
|
||||
@@ -25,11 +27,14 @@ import { getRandom } from '@/utils/helpers/safeRandom';
|
||||
import { BlockDto } from '../dto/block.dto';
|
||||
import { BlockRepository } from '../repositories/block.repository';
|
||||
import { Block, BlockFull, BlockPopulate } from '../schemas/block.schema';
|
||||
import { Label } from '../schemas/label.schema';
|
||||
import { Subscriber } from '../schemas/subscriber.schema';
|
||||
import { Context } from '../schemas/types/context';
|
||||
import {
|
||||
BlockMessage,
|
||||
OutgoingMessageFormat,
|
||||
StdOutgoingEnvelope,
|
||||
StdOutgoingSystemEnvelope,
|
||||
} from '../schemas/types/message';
|
||||
import { NlpPattern, PayloadPattern } from '../schemas/types/pattern';
|
||||
import { Payload, StdQuickReply } from '../schemas/types/quick-reply';
|
||||
@@ -55,10 +60,75 @@ export class BlockService extends BaseService<
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters an array of blocks based on the specified channel.
|
||||
*
|
||||
* This function ensures that only blocks that are either:
|
||||
* - Not restricted to specific trigger channels (`trigger_channels` is undefined or empty), or
|
||||
* - Explicitly allow the given channel (or the console channel)
|
||||
*
|
||||
* are included in the returned array.
|
||||
*
|
||||
* @param blocks - The list of blocks to be filtered.
|
||||
* @param channel - The name of the channel to filter blocks by.
|
||||
*
|
||||
* @returns The filtered array of blocks that are allowed for the given channel.
|
||||
*/
|
||||
filterBlocksByChannel<B extends Block | BlockFull>(
|
||||
blocks: B[],
|
||||
channel: ChannelName,
|
||||
) {
|
||||
return blocks.filter((b) => {
|
||||
return (
|
||||
!b.trigger_channels ||
|
||||
b.trigger_channels.length === 0 ||
|
||||
[...b.trigger_channels, CONSOLE_CHANNEL_NAME].includes(channel)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters an array of blocks based on subscriber labels.
|
||||
*
|
||||
* This function selects blocks that either:
|
||||
* - Have no trigger labels (making them applicable to all subscribers), or
|
||||
* - Contain at least one trigger label that matches a label from the provided list.
|
||||
*
|
||||
* The filtered blocks are then **sorted** in descending order by the number of trigger labels,
|
||||
* ensuring that blocks with more specific targeting (more trigger labels) are prioritized.
|
||||
*
|
||||
* @param blocks - The list of blocks to be filtered.
|
||||
* @param labels - The list of subscriber labels to match against.
|
||||
* @returns The filtered and sorted list of blocks.
|
||||
*/
|
||||
filterBlocksBySubscriberLabels<B extends Block | BlockFull>(
|
||||
blocks: B[],
|
||||
profile?: Subscriber,
|
||||
) {
|
||||
if (!profile) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
return (
|
||||
blocks
|
||||
.filter((b) => {
|
||||
const triggerLabels = b.trigger_labels.map((l) =>
|
||||
typeof l === 'string' ? l : l.id,
|
||||
);
|
||||
return (
|
||||
triggerLabels.length === 0 ||
|
||||
triggerLabels.some((l) => profile.labels.includes(l))
|
||||
);
|
||||
})
|
||||
// Priority goes to block who target users with labels
|
||||
.sort((a, b) => b.trigger_labels.length - a.trigger_labels.length)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a block whose patterns matches the received event
|
||||
*
|
||||
* @param blocks blocks Starting/Next blocks in the conversation flow
|
||||
* @param filteredBlocks blocks Starting/Next blocks in the conversation flow
|
||||
* @param event Received channel's message
|
||||
*
|
||||
* @returns The block that matches
|
||||
@@ -75,37 +145,15 @@ export class BlockService extends BaseService<
|
||||
let block: BlockFull | undefined = undefined;
|
||||
const payload = event.getPayload();
|
||||
|
||||
// Perform a filter on the specific channels
|
||||
const channel = event.getHandler().getName();
|
||||
blocks = blocks.filter((b) => {
|
||||
return (
|
||||
!b.trigger_channels ||
|
||||
b.trigger_channels.length === 0 ||
|
||||
[...b.trigger_channels, CONSOLE_CHANNEL_NAME].includes(channel)
|
||||
);
|
||||
});
|
||||
|
||||
// Perform a filter on trigger labels
|
||||
let userLabels: string[] = [];
|
||||
const profile = event.getSender();
|
||||
if (profile && Array.isArray(profile.labels)) {
|
||||
userLabels = profile.labels.map((l) => l);
|
||||
}
|
||||
|
||||
blocks = blocks
|
||||
.filter((b) => {
|
||||
const trigger_labels = b.trigger_labels.map(({ id }) => id);
|
||||
return (
|
||||
trigger_labels.length === 0 ||
|
||||
trigger_labels.some((l) => userLabels.includes(l))
|
||||
);
|
||||
})
|
||||
// Priority goes to block who target users with labels
|
||||
.sort((a, b) => b.trigger_labels.length - a.trigger_labels.length);
|
||||
// Perform a filter to get the candidates blocks
|
||||
const filteredBlocks = this.filterBlocksBySubscriberLabels(
|
||||
this.filterBlocksByChannel(blocks, event.getHandler().getName()),
|
||||
event.getSender(),
|
||||
);
|
||||
|
||||
// Perform a payload match & pick last createdAt
|
||||
if (payload) {
|
||||
block = blocks
|
||||
block = filteredBlocks
|
||||
.filter((b) => {
|
||||
return this.matchPayload(payload, b);
|
||||
})
|
||||
@@ -129,7 +177,7 @@ export class BlockService extends BaseService<
|
||||
}
|
||||
|
||||
// Perform a text pattern match
|
||||
block = blocks
|
||||
block = filteredBlocks
|
||||
.filter((b) => {
|
||||
return this.matchText(text, b);
|
||||
})
|
||||
@@ -139,7 +187,7 @@ export class BlockService extends BaseService<
|
||||
if (!block && nlp) {
|
||||
// Find block pattern having the best match of nlp entities
|
||||
let nlpBest = 0;
|
||||
blocks.forEach((b, index, self) => {
|
||||
filteredBlocks.forEach((b, index, self) => {
|
||||
const nlpPattern = this.matchNLP(nlp, b);
|
||||
if (nlpPattern && nlpPattern.length > nlpBest) {
|
||||
nlpBest = nlpPattern.length;
|
||||
@@ -293,6 +341,36 @@ export class BlockService extends BaseService<
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches an outcome-based block from a list of available blocks
|
||||
* based on the outcome of a system message.
|
||||
*
|
||||
* @param blocks - An array of blocks to search for a matching outcome.
|
||||
* @param envelope - The system message envelope containing the outcome to match.
|
||||
*
|
||||
* @returns - Returns the first matching block if found, otherwise returns `undefined`.
|
||||
*/
|
||||
matchOutcome(
|
||||
blocks: Block[],
|
||||
event: EventWrapper<any, any>,
|
||||
envelope: StdOutgoingSystemEnvelope,
|
||||
) {
|
||||
// Perform a filter to get the candidates blocks
|
||||
const filteredBlocks = this.filterBlocksBySubscriberLabels(
|
||||
this.filterBlocksByChannel(blocks, event.getHandler().getName()),
|
||||
event.getSender(),
|
||||
);
|
||||
return filteredBlocks.find((b) => {
|
||||
return b.patterns
|
||||
.filter(
|
||||
(p) => typeof p === 'object' && 'type' in p && p.type === 'outcome',
|
||||
)
|
||||
.some((p: PayloadPattern) =>
|
||||
['any', envelope.message.outcome].includes(p.value),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces tokens with their context variables values in the provided text message
|
||||
*
|
||||
@@ -604,4 +682,32 @@ export class BlockService extends BaseService<
|
||||
}
|
||||
throw new Error('Invalid message format.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `trigger_labels` and `assign_labels` fields of a block when a label is deleted.
|
||||
*
|
||||
*
|
||||
* This method removes the deleted label from the `trigger_labels` and `assign_labels` fields of all blocks that have the label.
|
||||
*
|
||||
* @param label The label that is being deleted.
|
||||
*/
|
||||
@OnEvent('hook:label:delete')
|
||||
async handleLabelDelete(labels: Label[]) {
|
||||
const blocks = await this.find({
|
||||
$or: [
|
||||
{ trigger_labels: { $in: labels.map((l) => l.id) } },
|
||||
{ assign_labels: { $in: labels.map((l) => l.id) } },
|
||||
],
|
||||
});
|
||||
|
||||
for (const block of blocks) {
|
||||
const trigger_labels = block.trigger_labels.filter(
|
||||
(labelId) => !labels.find((l) => l.id === labelId),
|
||||
);
|
||||
const assign_labels = block.assign_labels.filter(
|
||||
(labelId) => !labels.find((l) => l.id === labelId),
|
||||
);
|
||||
await this.updateOne(block.id, { trigger_labels, assign_labels });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -201,7 +201,7 @@ describe('BlockService', () => {
|
||||
|
||||
let hasBotSpoken = false;
|
||||
const clearMock = jest
|
||||
.spyOn(botService, 'findBlockAndSendReply')
|
||||
.spyOn(botService, 'triggerBlock')
|
||||
.mockImplementation(
|
||||
(
|
||||
actualEvent: WebEventWrapper<typeof WEB_CHANNEL_NAME>,
|
||||
|
||||
@@ -21,8 +21,11 @@ import {
|
||||
getDefaultConversationContext,
|
||||
} from '../schemas/conversation.schema';
|
||||
import { Context } from '../schemas/types/context';
|
||||
import { IncomingMessageType } from '../schemas/types/message';
|
||||
import { SubscriberContext } from '../schemas/types/subscriberContext';
|
||||
import {
|
||||
IncomingMessageType,
|
||||
OutgoingMessageFormat,
|
||||
StdOutgoingMessageEnvelope,
|
||||
} from '../schemas/types/message';
|
||||
|
||||
import { BlockService } from './block.service';
|
||||
import { ConversationService } from './conversation.service';
|
||||
@@ -40,40 +43,24 @@ export class BotService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sends a processed message to the user based on a specified content block.
|
||||
* Replaces tokens within the block with context data, handles fallback scenarios,
|
||||
* and assigns relevant labels to the user.
|
||||
* Sends a message to the subscriber via the appropriate messaging channel and handles related events.
|
||||
*
|
||||
* @param event - The incoming message or action that triggered the bot's response.
|
||||
* @param envelope - The outgoing message envelope containing the bot's response.
|
||||
* @param block - The content block containing the message and options to be sent.
|
||||
* @param context - Optional. The conversation context object, containing relevant data for personalization.
|
||||
* @param fallback - Optional. Boolean flag indicating if this is a fallback message when no appropriate response was found.
|
||||
* @param conversationId - Optional. The conversation ID to link the message to a specific conversation thread.
|
||||
*
|
||||
* @returns A promise that resolves with the message response, including the message ID.
|
||||
*/
|
||||
async sendMessageToSubscriber(
|
||||
envelope: StdOutgoingMessageEnvelope,
|
||||
event: EventWrapper<any, any>,
|
||||
block: BlockFull,
|
||||
context?: Context,
|
||||
fallback?: boolean,
|
||||
conservationId?: string,
|
||||
) {
|
||||
context = context || getDefaultConversationContext();
|
||||
fallback = typeof fallback !== 'undefined' ? fallback : false;
|
||||
const options = block.options;
|
||||
this.logger.debug('Sending message ... ', event.getSenderForeignId());
|
||||
// Process message : Replace tokens with context data and then send the message
|
||||
const recipient = event.getSender();
|
||||
const envelope = await this.blockService.processMessage(
|
||||
block,
|
||||
context,
|
||||
recipient?.context as SubscriberContext,
|
||||
fallback,
|
||||
conservationId,
|
||||
);
|
||||
// Send message through the right channel
|
||||
|
||||
this.logger.debug('Sending message ... ', event.getSenderForeignId());
|
||||
const response = await event
|
||||
.getHandler()
|
||||
.sendMessage(event, envelope, options, context);
|
||||
@@ -114,35 +101,56 @@ export class BotService {
|
||||
);
|
||||
|
||||
this.logger.debug('Assigned labels ', blockLabels);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an appropriate reply block and sends it to the user.
|
||||
* If there are additional blocks or attached blocks, it continues the conversation flow.
|
||||
* Ends the conversation if no further blocks are available.
|
||||
* Processes and executes a block, handling its associated messages and flow logic.
|
||||
*
|
||||
* The function performs the following steps:
|
||||
* 1. Retrieves the conversation context and recipient information.
|
||||
* 2. Generates an outgoing message envelope from the block.
|
||||
* 3. Sends the message to the subscriber unless it's a system message.
|
||||
* 4. Handles block chaining:
|
||||
* - If the block has an attached block, it recursively triggers the attached block.
|
||||
* - If the block has multiple possible next blocks, it determines the next block based on the outcome of the system message.
|
||||
* - If there are next blocks but no outcome-based matching, it updates the conversation state for the next steps.
|
||||
* 5. If no further blocks exist, it ends the flow execution.
|
||||
*
|
||||
* @param event - The incoming message or action that initiated this response.
|
||||
* @param convo - The current conversation context and flow.
|
||||
* @param block - The content block to be processed and sent.
|
||||
* @param fallback - Boolean indicating if this is a fallback response in case no appropriate reply was found.
|
||||
*
|
||||
* @returns A promise that continues or ends the conversation based on available blocks.
|
||||
* @returns A promise that either continues or ends the flow execution based on the available blocks.
|
||||
*/
|
||||
async findBlockAndSendReply(
|
||||
async triggerBlock(
|
||||
event: EventWrapper<any, any>,
|
||||
convo: Conversation,
|
||||
block: BlockFull,
|
||||
fallback: boolean,
|
||||
fallback: boolean = false,
|
||||
) {
|
||||
try {
|
||||
await this.sendMessageToSubscriber(
|
||||
event,
|
||||
const context = convo.context || getDefaultConversationContext();
|
||||
const recipient = event.getSender();
|
||||
|
||||
const envelope = await this.blockService.processMessage(
|
||||
block,
|
||||
convo.context,
|
||||
context,
|
||||
recipient?.context,
|
||||
fallback,
|
||||
convo.id,
|
||||
);
|
||||
|
||||
if (envelope.format !== OutgoingMessageFormat.system) {
|
||||
await this.sendMessageToSubscriber(
|
||||
envelope,
|
||||
event,
|
||||
block,
|
||||
context,
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
|
||||
if (block.attachedBlock) {
|
||||
// Sequential messaging ?
|
||||
try {
|
||||
@@ -154,12 +162,7 @@ export class BotService {
|
||||
'No attached block to be found with id ' + block.attachedBlock,
|
||||
);
|
||||
}
|
||||
return await this.findBlockAndSendReply(
|
||||
event,
|
||||
convo,
|
||||
attachedBlock,
|
||||
fallback,
|
||||
);
|
||||
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);
|
||||
@@ -168,20 +171,47 @@ export class BotService {
|
||||
Array.isArray(block.nextBlocks) &&
|
||||
block.nextBlocks.length > 0
|
||||
) {
|
||||
// Conversation continues : Go forward to next blocks
|
||||
this.logger.debug('Conversation continues ...', convo.id);
|
||||
const nextIds = block.nextBlocks.map(({ id }) => id);
|
||||
try {
|
||||
await this.conversationService.updateOne(convo.id, {
|
||||
current: block.id,
|
||||
next: nextIds,
|
||||
});
|
||||
if (envelope.format === OutgoingMessageFormat.system) {
|
||||
// System message: Trigger the next block based on the outcome
|
||||
this.logger.debug(
|
||||
'Matching the outcome against the next blocks ...',
|
||||
convo.id,
|
||||
);
|
||||
const match = this.blockService.matchOutcome(
|
||||
block.nextBlocks,
|
||||
event,
|
||||
envelope,
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const nextBlock = await this.blockService.findOneAndPopulate(
|
||||
match.id,
|
||||
);
|
||||
if (!nextBlock) {
|
||||
throw new Error(
|
||||
'No attached block to be found with id ' +
|
||||
block.attachedBlock,
|
||||
);
|
||||
}
|
||||
return await this.triggerBlock(event, convo, nextBlock, fallback);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'Block outcome did not match any of the next blocks',
|
||||
convo,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Conversation continues : Go forward to next blocks
|
||||
this.logger.debug('Conversation continues ...', convo.id);
|
||||
const nextIds = block.nextBlocks.map(({ id }) => id);
|
||||
await this.conversationService.updateOne(convo.id, {
|
||||
current: block.id,
|
||||
next: nextIds,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
'Unable to update conversation when going next',
|
||||
convo,
|
||||
err,
|
||||
);
|
||||
this.logger.error('Unable to continue the flow', convo, err);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -275,12 +305,7 @@ export class BotService {
|
||||
// Otherwise, old captured const value may be replaced by another const value
|
||||
!fallback,
|
||||
);
|
||||
await this.findBlockAndSendReply(
|
||||
event,
|
||||
updatedConversation,
|
||||
next,
|
||||
fallback,
|
||||
);
|
||||
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);
|
||||
@@ -376,12 +401,7 @@ export class BotService {
|
||||
subscriber.id,
|
||||
block.name,
|
||||
);
|
||||
return this.findBlockAndSendReply(
|
||||
event,
|
||||
updatedConversation,
|
||||
block,
|
||||
false,
|
||||
);
|
||||
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);
|
||||
@@ -459,7 +479,7 @@ export class BotService {
|
||||
'No global fallback block defined, sending a message ...',
|
||||
err,
|
||||
);
|
||||
this.sendMessageToSubscriber(event, {
|
||||
const globalFallbackBlock = {
|
||||
id: 'global-fallback',
|
||||
name: 'Global Fallback',
|
||||
message: settings.chatbot_settings.fallback_message,
|
||||
@@ -473,7 +493,19 @@ export class BotService {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
attachedBlock: null,
|
||||
} as any as BlockFull);
|
||||
} as any as BlockFull;
|
||||
|
||||
const envelope = await this.blockService.processMessage(
|
||||
globalFallbackBlock,
|
||||
getDefaultConversationContext(),
|
||||
{ vars: {} }, // @TODO: use subscriber ctx
|
||||
);
|
||||
|
||||
await this.sendMessageToSubscriber(
|
||||
envelope as StdOutgoingMessageEnvelope,
|
||||
event,
|
||||
globalFallbackBlock,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Do nothing ...
|
||||
|
||||
@@ -14,8 +14,10 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { InvitationRepository } from '@/user/repositories/invitation.repository';
|
||||
import { RoleRepository } from '@/user/repositories/role.repository';
|
||||
import { UserRepository } from '@/user/repositories/user.repository';
|
||||
import { InvitationModel } from '@/user/schemas/invitation.schema';
|
||||
import { PermissionModel } from '@/user/schemas/permission.schema';
|
||||
import { RoleModel } from '@/user/schemas/role.schema';
|
||||
import { User, UserModel } from '@/user/schemas/user.schema';
|
||||
@@ -62,6 +64,7 @@ describe('MessageService', () => {
|
||||
UserModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
SubscriberModel,
|
||||
MessageModel,
|
||||
AttachmentModel,
|
||||
@@ -75,6 +78,7 @@ describe('MessageService', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
SubscriberService,
|
||||
SubscriberRepository,
|
||||
MessageService,
|
||||
|
||||
@@ -14,8 +14,10 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { InvitationRepository } from '@/user/repositories/invitation.repository';
|
||||
import { RoleRepository } from '@/user/repositories/role.repository';
|
||||
import { UserRepository } from '@/user/repositories/user.repository';
|
||||
import { InvitationModel } from '@/user/schemas/invitation.schema';
|
||||
import { PermissionModel } from '@/user/schemas/permission.schema';
|
||||
import { RoleModel } from '@/user/schemas/role.schema';
|
||||
import { User, UserModel } from '@/user/schemas/user.schema';
|
||||
@@ -55,6 +57,7 @@ describe('SubscriberService', () => {
|
||||
LabelModel,
|
||||
UserModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
@@ -68,6 +71,7 @@ describe('SubscriberService', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
AttachmentService,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { WebsocketGateway } from '@/websocket/websocket.gateway';
|
||||
|
||||
import { SubscriberDto, SubscriberUpdateDto } from '../dto/subscriber.dto';
|
||||
import { SubscriberRepository } from '../repositories/subscriber.repository';
|
||||
import { Label } from '../schemas/label.schema';
|
||||
import {
|
||||
Subscriber,
|
||||
SubscriberFull,
|
||||
@@ -208,4 +209,24 @@ export class SubscriberService extends BaseService<
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `labels` field of a subscriber when a label is deleted.
|
||||
*
|
||||
* This method removes the deleted label from the `labels` field of all subscribers that have the label.
|
||||
*
|
||||
* @param label The label that is being deleted.
|
||||
*/
|
||||
@OnEvent('hook:label:delete')
|
||||
async handleLabelDelete(labels: Label[]) {
|
||||
const subscribers = await this.find({
|
||||
labels: { $in: labels.map((l) => l.id) },
|
||||
});
|
||||
for (const subscriber of subscribers) {
|
||||
const updatedLabels = subscriber.labels.filter(
|
||||
(label) => !labels.find((l) => l.id === label),
|
||||
);
|
||||
await this.updateOne(subscriber.id, { labels: updatedLabels });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 +9,8 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
|
||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||
import { ChatModule } from '@/chat/chat.module';
|
||||
|
||||
import { AttachmentModel } from '../attachment/schemas/attachment.schema';
|
||||
|
||||
import { ContentTypeController } from './controllers/content-type.controller';
|
||||
import { ContentController } from './controllers/content.controller';
|
||||
import { MenuController } from './controllers/menu.controller';
|
||||
@@ -29,13 +26,7 @@ import { MenuService } from './services/menu.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
ContentModel,
|
||||
ContentTypeModel,
|
||||
AttachmentModel,
|
||||
MenuModel,
|
||||
]),
|
||||
AttachmentModule,
|
||||
MongooseModule.forFeature([ContentModel, ContentTypeModel, MenuModel]),
|
||||
forwardRef(() => ChatModule),
|
||||
],
|
||||
controllers: [ContentController, ContentTypeController, MenuController],
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import { NotFoundException } from '@nestjs/common/exceptions';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentModel,
|
||||
} from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
@@ -48,10 +40,8 @@ describe('ContentController', () => {
|
||||
let contentController: ContentController;
|
||||
let contentService: ContentService;
|
||||
let contentTypeService: ContentTypeService;
|
||||
let attachmentService: AttachmentService;
|
||||
let contentType: ContentType | null;
|
||||
let content: Content | null;
|
||||
let attachment: Attachment | null;
|
||||
let updatedContent;
|
||||
let pageQuery: PageQueryDto<Content>;
|
||||
|
||||
@@ -60,34 +50,24 @@ describe('ContentController', () => {
|
||||
controllers: [ContentController],
|
||||
imports: [
|
||||
rootMongooseTestModule(installContentFixtures),
|
||||
MongooseModule.forFeature([
|
||||
ContentTypeModel,
|
||||
ContentModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
MongooseModule.forFeature([ContentTypeModel, ContentModel]),
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
ContentTypeService,
|
||||
ContentService,
|
||||
ContentRepository,
|
||||
AttachmentService,
|
||||
ContentTypeRepository,
|
||||
AttachmentRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
contentController = module.get<ContentController>(ContentController);
|
||||
contentService = module.get<ContentService>(ContentService);
|
||||
attachmentService = module.get<AttachmentService>(AttachmentService);
|
||||
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
|
||||
contentType = await contentTypeService.findOne({ name: 'Product' });
|
||||
content = await contentService.findOne({
|
||||
title: 'Jean',
|
||||
});
|
||||
attachment = await attachmentService.findOne({
|
||||
name: 'store1.jpg',
|
||||
});
|
||||
|
||||
pageQuery = getPageQuery<Content>({
|
||||
limit: 1,
|
||||
@@ -243,91 +223,74 @@ describe('ContentController', () => {
|
||||
});
|
||||
|
||||
describe('import', () => {
|
||||
const mockCsvData: string = `other,title,status,image
|
||||
should not appear,store 3,true,image.jpg`;
|
||||
|
||||
const file: Express.Multer.File = {
|
||||
buffer: Buffer.from(mockCsvData, 'utf-8'),
|
||||
originalname: 'test.csv',
|
||||
mimetype: 'text/csv',
|
||||
size: mockCsvData.length,
|
||||
fieldname: 'file',
|
||||
encoding: '7bit',
|
||||
stream: null,
|
||||
destination: '',
|
||||
filename: '',
|
||||
path: '',
|
||||
} as unknown as Express.Multer.File;
|
||||
|
||||
it('should import content from a CSV file', async () => {
|
||||
const mockCsvData: string = `other,title,status,image
|
||||
should not appear,store 3,true,image.jpg`;
|
||||
|
||||
const mockCsvContentDto: ContentCreateDto = {
|
||||
entity: '0',
|
||||
title: 'store 3',
|
||||
status: true,
|
||||
dynamicFields: {
|
||||
image: 'image.jpg',
|
||||
},
|
||||
};
|
||||
jest.spyOn(contentService, 'createMany');
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true);
|
||||
jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockCsvData);
|
||||
|
||||
const contentType = await contentTypeService.findOne({
|
||||
const mockContentType = {
|
||||
id: '0',
|
||||
name: 'Store',
|
||||
});
|
||||
|
||||
const result = await contentController.import({
|
||||
idFileToImport: attachment!.id,
|
||||
idTargetContentType: contentType!.id,
|
||||
});
|
||||
expect(contentService.createMany).toHaveBeenCalledWith([
|
||||
{ ...mockCsvContentDto, entity: contentType!.id },
|
||||
} as unknown as ContentType;
|
||||
jest
|
||||
.spyOn(contentTypeService, 'findOne')
|
||||
.mockResolvedValueOnce(mockContentType);
|
||||
jest.spyOn(contentService, 'parseAndSaveDataset').mockResolvedValueOnce([
|
||||
{
|
||||
entity: mockContentType.id,
|
||||
title: 'store 3',
|
||||
status: true,
|
||||
dynamicFields: {
|
||||
image: 'image.jpg',
|
||||
},
|
||||
id: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqualPayload(
|
||||
[
|
||||
{
|
||||
...mockCsvContentDto,
|
||||
entity: contentType!.id,
|
||||
},
|
||||
],
|
||||
[...IGNORED_TEST_FIELDS, 'rag'],
|
||||
const result = await contentController.import(file, mockContentType.id);
|
||||
expect(contentService.parseAndSaveDataset).toHaveBeenCalledWith(
|
||||
mockCsvData,
|
||||
mockContentType.id,
|
||||
mockContentType,
|
||||
);
|
||||
expect(result).toEqualPayload([
|
||||
{
|
||||
entity: mockContentType.id,
|
||||
title: 'store 3',
|
||||
status: true,
|
||||
dynamicFields: {
|
||||
image: 'image.jpg',
|
||||
},
|
||||
id: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if content type is not found', async () => {
|
||||
jest.spyOn(contentTypeService, 'findOne').mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
contentController.import({
|
||||
idFileToImport: attachment!.id,
|
||||
idTargetContentType: NOT_FOUND_ID,
|
||||
}),
|
||||
contentController.import(file, 'INVALID_ID'),
|
||||
).rejects.toThrow(new NotFoundException('Content type is not found'));
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if file is not found in attachment database', async () => {
|
||||
const contentType = await contentTypeService.findOne({
|
||||
name: 'Product',
|
||||
});
|
||||
jest.spyOn(contentTypeService, 'findOne');
|
||||
await expect(
|
||||
contentController.import({
|
||||
idFileToImport: NOT_FOUND_ID,
|
||||
idTargetContentType: contentType!.id.toString(),
|
||||
}),
|
||||
).rejects.toThrow(new NotFoundException('File does not exist'));
|
||||
it('should throw NotFoundException if idTargetContentType is missing', async () => {
|
||||
await expect(contentController.import(file, '')).rejects.toThrow(
|
||||
new NotFoundException('Missing parameter'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if file does not exist in the given path ', async () => {
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
|
||||
await expect(
|
||||
contentController.import({
|
||||
idFileToImport: attachment!.id,
|
||||
idTargetContentType: contentType!.id,
|
||||
}),
|
||||
).rejects.toThrow(new NotFoundException('File does not exist'));
|
||||
});
|
||||
|
||||
it.each([
|
||||
['file param and content type params are missing', '', ''],
|
||||
['content type param is missing', '', NOT_FOUND_ID],
|
||||
['file param is missing', NOT_FOUND_ID, ''],
|
||||
])(
|
||||
'should throw NotFoundException if %s',
|
||||
async (_message, fileToImport, targetContentType) => {
|
||||
await expect(
|
||||
contentController.import({
|
||||
idFileToImport: fileToImport,
|
||||
idTargetContentType: targetContentType,
|
||||
}),
|
||||
).rejects.toThrow(new NotFoundException('Missing params'));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@@ -20,14 +17,12 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { BadRequestException } from '@nestjs/common/exceptions';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { config } from '@/config';
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
@@ -59,7 +54,6 @@ export class ContentController extends BaseController<
|
||||
constructor(
|
||||
private readonly contentService: ContentService,
|
||||
private readonly contentTypeService: ContentTypeService,
|
||||
private readonly attachmentService: AttachmentService,
|
||||
private readonly logger: LoggerService,
|
||||
) {
|
||||
super(contentService);
|
||||
@@ -92,29 +86,22 @@ export class ContentController extends BaseController<
|
||||
/**
|
||||
* Imports content from a CSV file based on the provided content type and file ID.
|
||||
*
|
||||
* @param idTargetContentType - The content type to match the CSV data against.
|
||||
* @param idFileToImport - The ID of the file to be imported.
|
||||
*
|
||||
* @param idTargetContentType - The content type to match the CSV data against. *
|
||||
* @returns A promise that resolves to the newly created content documents.
|
||||
*/
|
||||
@Get('import/:idTargetContentType/:idFileToImport')
|
||||
@CsrfCheck(true)
|
||||
@Post('import')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async import(
|
||||
@Param()
|
||||
{
|
||||
idTargetContentType: targetContentType,
|
||||
idFileToImport: fileToImport,
|
||||
}: {
|
||||
idTargetContentType: string;
|
||||
idFileToImport: string;
|
||||
},
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Query('idTargetContentType')
|
||||
targetContentType: string,
|
||||
) {
|
||||
// Check params
|
||||
if (!fileToImport || !targetContentType) {
|
||||
this.logger.warn(`Parameters are missing`);
|
||||
throw new NotFoundException(`Missing params`);
|
||||
const datasetContent = file.buffer.toString('utf-8');
|
||||
if (!targetContentType) {
|
||||
this.logger.warn(`Parameter is missing`);
|
||||
throw new NotFoundException(`Missing parameter`);
|
||||
}
|
||||
|
||||
// Find the content type that corresponds to the given content
|
||||
const contentType =
|
||||
await this.contentTypeService.findOne(targetContentType);
|
||||
if (!contentType) {
|
||||
@@ -124,56 +111,11 @@ export class ContentController extends BaseController<
|
||||
throw new NotFoundException(`Content type is not found`);
|
||||
}
|
||||
|
||||
// Get file location
|
||||
const file = await this.attachmentService.findOne(fileToImport);
|
||||
// Check if file is present
|
||||
const filePath = file
|
||||
? path.join(config.parameters.uploadDir, file.location)
|
||||
: undefined;
|
||||
|
||||
if (!file || !filePath || !fs.existsSync(filePath)) {
|
||||
this.logger.warn(`Failed to find file type with id ${fileToImport}.`);
|
||||
throw new NotFoundException(`File does not exist`);
|
||||
}
|
||||
//read file sync
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const result = Papa.parse<Record<string, string | boolean | number>>(data, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
dynamicTyping: true,
|
||||
});
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
this.logger.warn(
|
||||
`Errors parsing the file: ${JSON.stringify(result.errors)}`,
|
||||
);
|
||||
|
||||
throw new BadRequestException(result.errors, {
|
||||
cause: result.errors,
|
||||
description: 'Error while parsing CSV',
|
||||
});
|
||||
}
|
||||
|
||||
const contentsDto = result.data.reduce(
|
||||
(acc, { title, status, ...rest }) => [
|
||||
...acc,
|
||||
{
|
||||
title: String(title),
|
||||
status: Boolean(status),
|
||||
entity: targetContentType,
|
||||
dynamicFields: Object.keys(rest)
|
||||
.filter((key) =>
|
||||
contentType.fields?.map((field) => field.name).includes(key),
|
||||
)
|
||||
.reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}),
|
||||
},
|
||||
],
|
||||
[],
|
||||
return await this.contentService.parseAndSaveDataset(
|
||||
datasetContent,
|
||||
targetContentType,
|
||||
contentType,
|
||||
);
|
||||
|
||||
// Create content
|
||||
return await this.contentService.createMany(contentsDto);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,9 +10,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { OutgoingMessageFormat } from '@/chat/schemas/types/message';
|
||||
import { ContentOptions } from '@/chat/schemas/types/options';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
@@ -44,19 +41,13 @@ describe('ContentService', () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installContentFixtures),
|
||||
MongooseModule.forFeature([
|
||||
ContentTypeModel,
|
||||
ContentModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
MongooseModule.forFeature([ContentTypeModel, ContentModel]),
|
||||
],
|
||||
providers: [
|
||||
ContentTypeRepository,
|
||||
ContentRepository,
|
||||
AttachmentRepository,
|
||||
ContentTypeService,
|
||||
ContentService,
|
||||
AttachmentService,
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
],
|
||||
|
||||
@@ -6,9 +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 { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { StdOutgoingListMessage } from '@/chat/schemas/types/message';
|
||||
import { ContentOptions } from '@/chat/schemas/types/options';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
@@ -17,6 +17,7 @@ import { TFilterQuery } from '@/utils/types/filter.types';
|
||||
|
||||
import { ContentDto } from '../dto/content.dto';
|
||||
import { ContentRepository } from '../repositories/content.repository';
|
||||
import { ContentType } from '../schemas/content-type.schema';
|
||||
import {
|
||||
Content,
|
||||
ContentFull,
|
||||
@@ -32,7 +33,6 @@ export class ContentService extends BaseService<
|
||||
> {
|
||||
constructor(
|
||||
readonly repository: ContentRepository,
|
||||
private readonly attachmentService: AttachmentService,
|
||||
private readonly logger: LoggerService,
|
||||
) {
|
||||
super(repository);
|
||||
@@ -103,4 +103,68 @@ export class ContentService extends BaseService<
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CSV dataset and saves the content in the repository.
|
||||
*
|
||||
* @param data - The CSV data as a string to be parsed.
|
||||
* @param targetContentType - The content type to associate with the parsed data.
|
||||
* @param contentType - The content type metadata, including fields to validate the parsed data.
|
||||
* @return A promise resolving to the created content objects.
|
||||
*/
|
||||
async parseAndSaveDataset(
|
||||
data: string,
|
||||
targetContentType: string,
|
||||
contentType: ContentType,
|
||||
) {
|
||||
// Parse local CSV file
|
||||
const result: {
|
||||
errors: any[];
|
||||
data: Array<Record<string, string>>;
|
||||
} = Papa.parse(data, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
});
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
this.logger.warn(
|
||||
`Errors parsing the file: ${JSON.stringify(result.errors)}`,
|
||||
);
|
||||
throw new BadRequestException(result.errors, {
|
||||
cause: result.errors,
|
||||
description: 'Error while parsing CSV',
|
||||
});
|
||||
}
|
||||
if (!result.data.every((row) => row.title && row.status)) {
|
||||
throw new BadRequestException(
|
||||
'Missing required fields: "title" or "status"',
|
||||
{
|
||||
cause: 'Invalid CSV data',
|
||||
description: 'CSV must include "title" and "status" columns',
|
||||
},
|
||||
);
|
||||
}
|
||||
const contentsDto = result.data.reduce(
|
||||
(acc, { title, status, ...rest }) => [
|
||||
...acc,
|
||||
{
|
||||
title: String(title),
|
||||
status: Boolean(status),
|
||||
entity: targetContentType,
|
||||
dynamicFields: Object.keys(rest)
|
||||
.filter((key) =>
|
||||
contentType.fields?.map((field) => field.name).includes(key),
|
||||
)
|
||||
.reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
this.logger.log(`Parsed ${result.data.length} rows from CSV.`);
|
||||
try {
|
||||
return await this.createMany(contentsDto);
|
||||
} catch (err) {
|
||||
this.logger.error('Error occurred when extracting data. ', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
|
||||
import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants';
|
||||
import { Subscriber, SubscriberFull } from '@/chat/schemas/subscriber.schema';
|
||||
import { AttachmentRef } from '@/chat/schemas/types/attachment';
|
||||
import { Button, ButtonType } from '@/chat/schemas/types/button';
|
||||
import { Button, ButtonType, PayloadType } from '@/chat/schemas/types/button';
|
||||
import {
|
||||
AnyMessage,
|
||||
ContentElement,
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
IncomingMessageType,
|
||||
OutgoingMessage,
|
||||
OutgoingMessageFormat,
|
||||
PayloadType,
|
||||
StdEventType,
|
||||
StdOutgoingAttachmentMessage,
|
||||
StdOutgoingButtonsMessage,
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import { PayloadType } from '@/chat/schemas/types/button';
|
||||
import {
|
||||
IncomingMessageType,
|
||||
PayloadType,
|
||||
StdEventType,
|
||||
StdIncomingMessage,
|
||||
} from '@/chat/schemas/types/message';
|
||||
|
||||
@@ -136,6 +136,7 @@ describe('TranslationService', () => {
|
||||
const block: Block = {
|
||||
name: 'Ollama Plugin',
|
||||
patterns: [],
|
||||
outcomes: [],
|
||||
assign_labels: [],
|
||||
trigger_channels: [],
|
||||
trigger_labels: [],
|
||||
|
||||
12
api/src/setting/index.d.ts
vendored
12
api/src/setting/index.d.ts
vendored
@@ -1,11 +1,13 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { SettingType } from '@/setting/schemas/types';
|
||||
|
||||
import { SettingByType } from './schemas/types';
|
||||
import { DEFAULT_SETTINGS } from './seeds/setting.seed-model';
|
||||
|
||||
@@ -22,10 +24,14 @@ declare global {
|
||||
? { [K in keyof T]: TNativeType<T[K]> }
|
||||
: T;
|
||||
|
||||
type SettingValue<K> = K['type'] extends SettingType.select
|
||||
? K['options'][number]
|
||||
: TNativeType<K['value']>;
|
||||
|
||||
type SettingObject<
|
||||
T extends Omit<Setting, 'id' | 'createdAt' | 'updatedAt'>[],
|
||||
> = {
|
||||
[K in T[number] as K['label']]: TNativeType<K['value']>;
|
||||
[K in T[number] as K['label']]: SettingValue<K>;
|
||||
};
|
||||
|
||||
type SettingMapByType<
|
||||
@@ -38,7 +44,7 @@ declare global {
|
||||
T extends Omit<Setting, 'id' | 'createdAt' | 'updatedAt'>[],
|
||||
> = {
|
||||
[G in T[number] as G['group']]: {
|
||||
[K in T[number] as K['label']]: TNativeType<K['value']>;
|
||||
[K in T[number] as K['label']]: SettingValue<K>;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { ModelRepository } from '../repositories/model.repository';
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { ModelFull, ModelModel } from '../schemas/model.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { RoleModel } from '../schemas/role.schema';
|
||||
@@ -53,6 +55,7 @@ describe('ModelController', () => {
|
||||
UserModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
ModelModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
@@ -68,6 +71,7 @@ describe('ModelController', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
PermissionRepository,
|
||||
EventEmitter2,
|
||||
{
|
||||
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { PermissionCreateDto } from '../dto/permission.dto';
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { ModelRepository } from '../repositories/model.repository';
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { Model, ModelModel } from '../schemas/model.schema';
|
||||
import {
|
||||
Permission,
|
||||
@@ -53,7 +55,12 @@ describe('PermissionController', () => {
|
||||
controllers: [PermissionController],
|
||||
imports: [
|
||||
rootMongooseTestModule(installPermissionFixtures),
|
||||
MongooseModule.forFeature([PermissionModel, ModelModel, RoleModel]),
|
||||
MongooseModule.forFeature([
|
||||
PermissionModel,
|
||||
ModelModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
@@ -62,6 +69,7 @@ describe('PermissionController', () => {
|
||||
PermissionService,
|
||||
PermissionRepository,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
ModelRepository,
|
||||
EventEmitter2,
|
||||
{
|
||||
|
||||
@@ -26,9 +26,11 @@ import {
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { RoleCreateDto, RoleUpdateDto } from '../dto/role.dto';
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { Role, RoleFull, RoleModel } from '../schemas/role.schema';
|
||||
import { UserModel } from '../schemas/user.schema';
|
||||
@@ -55,6 +57,7 @@ describe('RoleController', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
UserModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
],
|
||||
@@ -65,6 +68,7 @@ describe('RoleController', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
PermissionRepository,
|
||||
EventEmitter2,
|
||||
AttachmentService,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { ModelRepository } from '../repositories/model.repository';
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { ModelModel, Model as ModelSchema } from '../schemas/model.schema';
|
||||
import {
|
||||
Permission,
|
||||
@@ -32,6 +33,8 @@ import {
|
||||
import { Role, RoleModel } from '../schemas/role.schema';
|
||||
import { Action } from '../types/action.type';
|
||||
|
||||
import { InvitationRepository } from './invitation.repository';
|
||||
|
||||
describe('PermissionRepository', () => {
|
||||
let modelRepository: ModelRepository;
|
||||
let roleRepository: RoleRepository;
|
||||
@@ -44,12 +47,18 @@ describe('PermissionRepository', () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installPermissionFixtures),
|
||||
MongooseModule.forFeature([ModelModel, PermissionModel, RoleModel]),
|
||||
MongooseModule.forFeature([
|
||||
ModelModel,
|
||||
PermissionModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
ModelRepository,
|
||||
RoleRepository,
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -21,11 +21,13 @@ import {
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { Role, RoleFull, RoleModel } from '../schemas/role.schema';
|
||||
import { User, UserModel } from '../schemas/user.schema';
|
||||
|
||||
import { roleFixtures } from './../../utils/test/fixtures/role';
|
||||
import { InvitationRepository } from './invitation.repository';
|
||||
|
||||
describe('RoleRepository', () => {
|
||||
let roleRepository: RoleRepository;
|
||||
@@ -40,11 +42,17 @@ describe('RoleRepository', () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installPermissionFixtures),
|
||||
MongooseModule.forFeature([UserModel, PermissionModel, RoleModel]),
|
||||
MongooseModule.forFeature([
|
||||
UserModel,
|
||||
PermissionModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
UserRepository,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
PermissionRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Model } from 'mongoose';
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
|
||||
import { RoleDto } from '../dto/role.dto';
|
||||
import { Invitation } from '../schemas/invitation.schema';
|
||||
import { Permission } from '../schemas/permission.schema';
|
||||
import {
|
||||
Role,
|
||||
@@ -34,6 +35,8 @@ export class RoleRepository extends BaseRepository<
|
||||
@InjectModel(Role.name) readonly model: Model<Role>,
|
||||
@InjectModel(Permission.name)
|
||||
private readonly permissionModel: Model<Permission>,
|
||||
@InjectModel(Invitation.name)
|
||||
private readonly invitationModel: Model<Invitation>,
|
||||
) {
|
||||
super(eventEmitter, model, Role, ROLE_POPULATE, RoleFull);
|
||||
}
|
||||
@@ -49,6 +52,7 @@ export class RoleRepository extends BaseRepository<
|
||||
const result = await this.model.deleteOne({ _id: id }).exec();
|
||||
if (result.deletedCount > 0) {
|
||||
await this.permissionModel.deleteMany({ role: id });
|
||||
await this.invitationModel.deleteMany({ roles: id });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,13 @@ import {
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { Role, RoleModel } from '../schemas/role.schema';
|
||||
import { User, UserFull, UserModel } from '../schemas/user.schema';
|
||||
|
||||
import { InvitationRepository } from './invitation.repository';
|
||||
|
||||
describe('UserRepository', () => {
|
||||
let roleRepository: RoleRepository;
|
||||
let userRepository: UserRepository;
|
||||
@@ -57,6 +60,7 @@ describe('UserRepository', () => {
|
||||
UserModel,
|
||||
PermissionModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
],
|
||||
@@ -64,6 +68,7 @@ describe('UserRepository', () => {
|
||||
LoggerService,
|
||||
UserRepository,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
PermissionRepository,
|
||||
EventEmitter2,
|
||||
{
|
||||
|
||||
@@ -21,8 +21,10 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { RoleModel } from '../schemas/role.schema';
|
||||
import { UserModel } from '../schemas/user.schema';
|
||||
@@ -43,6 +45,7 @@ describe('AuthService', () => {
|
||||
UserModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
],
|
||||
@@ -53,6 +56,7 @@ describe('AuthService', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
JwtService,
|
||||
EventEmitter2,
|
||||
AttachmentService,
|
||||
|
||||
@@ -32,8 +32,10 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { RoleModel } from '../schemas/role.schema';
|
||||
import { User, UserModel } from '../schemas/user.schema';
|
||||
@@ -60,6 +62,7 @@ describe('PasswordResetService', () => {
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
InvitationModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -70,6 +73,7 @@ describe('PasswordResetService', () => {
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
|
||||
@@ -21,9 +21,11 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { ModelRepository } from '../repositories/model.repository';
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { ModelModel, Model as ModelSchema } from '../schemas/model.schema';
|
||||
import {
|
||||
Permission,
|
||||
@@ -46,12 +48,18 @@ describe('PermissionService', () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installPermissionFixtures),
|
||||
MongooseModule.forFeature([ModelModel, PermissionModel, RoleModel]),
|
||||
MongooseModule.forFeature([
|
||||
ModelModel,
|
||||
PermissionModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
ModelRepository,
|
||||
PermissionService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
PermissionRepository,
|
||||
EventEmitter2,
|
||||
{
|
||||
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { Permission, PermissionModel } from '../schemas/permission.schema';
|
||||
import { Role, RoleFull, RoleModel } from '../schemas/role.schema';
|
||||
import { User, UserModel } from '../schemas/user.schema';
|
||||
@@ -40,12 +42,18 @@ describe('RoleService', () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installPermissionFixtures),
|
||||
MongooseModule.forFeature([UserModel, PermissionModel, RoleModel]),
|
||||
MongooseModule.forFeature([
|
||||
UserModel,
|
||||
PermissionModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
PermissionRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
|
||||
@@ -24,9 +24,11 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { Role, RoleModel } from '../schemas/role.schema';
|
||||
import { User, UserFull, UserModel } from '../schemas/user.schema';
|
||||
@@ -61,6 +63,7 @@ describe('UserService', () => {
|
||||
UserModel,
|
||||
PermissionModel,
|
||||
RoleModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
],
|
||||
@@ -73,6 +76,7 @@ describe('UserService', () => {
|
||||
PermissionService,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
PermissionRepository,
|
||||
EventEmitter2,
|
||||
{
|
||||
|
||||
@@ -29,8 +29,10 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { InvitationRepository } from '../repositories/invitation.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { InvitationModel } from '../schemas/invitation.schema';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { RoleModel } from '../schemas/role.schema';
|
||||
import { UserModel } from '../schemas/user.schema';
|
||||
@@ -53,6 +55,7 @@ describe('ValidateAccountService', () => {
|
||||
UserModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
@@ -65,6 +68,7 @@ describe('ValidateAccountService', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
InvitationRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
|
||||
5
api/src/utils/test/fixtures/block.ts
vendored
5
api/src/utils/test/fixtures/block.ts
vendored
@@ -35,6 +35,7 @@ export const blocks: TBlockFixtures['values'][] = [
|
||||
{
|
||||
name: 'hasNextBlocks',
|
||||
patterns: ['Hi'],
|
||||
outcomes: [],
|
||||
category: null,
|
||||
options: {
|
||||
typing: 0,
|
||||
@@ -53,6 +54,7 @@ export const blocks: TBlockFixtures['values'][] = [
|
||||
{
|
||||
name: 'hasPreviousBlocks',
|
||||
patterns: ['colors'],
|
||||
outcomes: [],
|
||||
category: null,
|
||||
options: {
|
||||
typing: 0,
|
||||
@@ -90,6 +92,7 @@ export const blocks: TBlockFixtures['values'][] = [
|
||||
{
|
||||
name: 'buttons',
|
||||
patterns: ['about'],
|
||||
outcomes: [],
|
||||
category: null,
|
||||
options: {
|
||||
typing: 0,
|
||||
@@ -127,6 +130,7 @@ export const blocks: TBlockFixtures['values'][] = [
|
||||
{
|
||||
name: 'attachment',
|
||||
patterns: ['image'],
|
||||
outcomes: [],
|
||||
category: null,
|
||||
options: {
|
||||
typing: 0,
|
||||
@@ -153,6 +157,7 @@ export const blocks: TBlockFixtures['values'][] = [
|
||||
{
|
||||
name: 'test',
|
||||
patterns: ['yes'],
|
||||
outcomes: [],
|
||||
category: null,
|
||||
//to be verified
|
||||
options: {
|
||||
|
||||
11
api/src/utils/test/fixtures/label.ts
vendored
11
api/src/utils/test/fixtures/label.ts
vendored
@@ -43,6 +43,17 @@ export const labels: TLabelFixtures['values'][] = [
|
||||
name: 'TEST_TITLE_2',
|
||||
title: 'test title 2',
|
||||
},
|
||||
{
|
||||
description: 'test description 3',
|
||||
label_id: {
|
||||
messenger: 'messenger',
|
||||
web: 'web',
|
||||
twitter: 'twitter',
|
||||
dimelo: 'dimelo',
|
||||
},
|
||||
name: 'TEST_TITLE_3',
|
||||
title: 'test title 3',
|
||||
},
|
||||
];
|
||||
|
||||
export const labelFixtures = getFixturesWithDefaultValues<
|
||||
|
||||
@@ -12,12 +12,9 @@ import {
|
||||
} from '@/channel/lib/__test__/label.mock';
|
||||
import { BlockFull } from '@/chat/schemas/block.schema';
|
||||
import { FileType } from '@/chat/schemas/types/attachment';
|
||||
import { ButtonType } from '@/chat/schemas/types/button';
|
||||
import { ButtonType, PayloadType } from '@/chat/schemas/types/button';
|
||||
import { CaptureVar } from '@/chat/schemas/types/capture-var';
|
||||
import {
|
||||
OutgoingMessageFormat,
|
||||
PayloadType,
|
||||
} from '@/chat/schemas/types/message';
|
||||
import { OutgoingMessageFormat } from '@/chat/schemas/types/message';
|
||||
import { BlockOptions, ContentOptions } from '@/chat/schemas/types/options';
|
||||
import { Pattern } from '@/chat/schemas/types/pattern';
|
||||
import { QuickReplyType } from '@/chat/schemas/types/quick-reply';
|
||||
|
||||
@@ -70,6 +70,7 @@ module.exports = {
|
||||
],
|
||||
2,
|
||||
],
|
||||
"no-multiple-empty-lines": ["error", { max: 1 }],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "hexabot-ui",
|
||||
"private": true,
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.5",
|
||||
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
|
||||
"author": "Hexastack",
|
||||
"license": "AGPL-3.0-only",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"invalid_file_type": "Invalid file type. Please select a file in the supported format.",
|
||||
"select_category": "Select a flow",
|
||||
"logout_failed": "Something went wrong during logout",
|
||||
"duplicate_labels_not_allowed": "Duplicate labels are not allowed"
|
||||
"duplicate_labels_not_allowed": "Duplicate labels are not allowed",
|
||||
"duplicate_block_error": "Something went wrong while duplicating block"
|
||||
},
|
||||
"menu": {
|
||||
"terms": "Terms of Use",
|
||||
@@ -248,6 +249,13 @@
|
||||
"triggers": "Triggers",
|
||||
"payloads": "Payloads",
|
||||
"general_payloads": "General Payloads",
|
||||
"exact_match": "Exact Match",
|
||||
"pattern_match": "Pattern Match",
|
||||
"intent_match": "Intent Match",
|
||||
"interaction": "Interaction",
|
||||
"outcome_match": "Outcome Match",
|
||||
"outcome": "Outcome",
|
||||
"any_outcome": "Any Outcome",
|
||||
"capture": "Capture?",
|
||||
"context_var": "Context Var",
|
||||
"text_message": "Text message",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"invalid_file_type": "Type de fichier invalide. Veuillez choisir un fichier dans un format pris en charge.",
|
||||
"select_category": "Sélectionner une catégorie",
|
||||
"logout_failed": "Une erreur s'est produite lors de la déconnexion",
|
||||
"duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées"
|
||||
"duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées",
|
||||
"duplicate_block_error": "Une erreur est survenue lors de la duplication du bloc"
|
||||
},
|
||||
"menu": {
|
||||
"terms": "Conditions d'utilisation",
|
||||
@@ -248,6 +249,13 @@
|
||||
"triggers": "Déclencheurs",
|
||||
"payloads": "Payloads",
|
||||
"general_payloads": "Payloads généraux",
|
||||
"exact_match": "Comparaison Exacte",
|
||||
"pattern_match": "Expression Régulière",
|
||||
"intent_match": "Intention",
|
||||
"interaction": "Interaction",
|
||||
"outcome": "Résultat",
|
||||
"outcome_match": "Résultat",
|
||||
"any_outcome": "N'importe quel résultat",
|
||||
"capture": "Capturer?",
|
||||
"context_var": "Variable contextuelle",
|
||||
"text_message": "Message texte",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { Box, FormHelperText, FormLabel } from "@mui/material";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
const dialogs = useDialogs();
|
||||
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: uploadAttachment } = useUpload(EntityType.ATTACHMENT, {
|
||||
const { mutate: uploadAttachment } = useUpload(EntityType.ATTACHMENT, {
|
||||
onError: () => {
|
||||
toast.error(t("message.upload_failed"));
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { Box, Button, FormHelperText, FormLabel } from "@mui/material";
|
||||
import { forwardRef, useState } from "react";
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -38,7 +38,7 @@ export const Login = () => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { authenticate } = useAuth();
|
||||
const { mutateAsync: login, isLoading } = useLogin({
|
||||
const { mutate: login, isLoading } = useLogin({
|
||||
onSuccess: (data) => {
|
||||
if (data.state) authenticate(data);
|
||||
else {
|
||||
@@ -49,7 +49,7 @@ export const Login = () => {
|
||||
toast.error(t("message.login_failure"));
|
||||
},
|
||||
});
|
||||
const { mutateAsync: confirmAccount } = useConfirmAccount({
|
||||
const { mutate: confirmAccount } = useConfirmAccount({
|
||||
onSuccess: () => {
|
||||
toast.success(t("message.reset_confirm_success"));
|
||||
},
|
||||
@@ -76,8 +76,8 @@ export const Login = () => {
|
||||
required: t("message.password_is_required"),
|
||||
},
|
||||
};
|
||||
const onSubmitForm = async (data: ILoginAttributes) => {
|
||||
await login(data);
|
||||
const onSubmitForm = (data: ILoginAttributes) => {
|
||||
login(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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.
|
||||
@@ -53,7 +53,7 @@ export const Register = () => {
|
||||
const { t } = useTranslate();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: acceptInvite, isLoading } = useAcceptInvite({
|
||||
const { mutate: acceptInvite, isLoading } = useAcceptInvite({
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
@@ -108,11 +108,11 @@ export const Register = () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const onSubmitForm = async ({
|
||||
const onSubmitForm = ({
|
||||
password2: _password2,
|
||||
...rest
|
||||
}: TRegisterExtendedPayload) => {
|
||||
await acceptInvite(rest);
|
||||
acceptInvite(rest);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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.
|
||||
@@ -74,14 +74,7 @@ const AutoCompleteEntitySelect = <
|
||||
const idRef = useRef(generateId());
|
||||
const params = {
|
||||
where: {
|
||||
or: [
|
||||
...(searchPayload.where.or || []),
|
||||
...(value
|
||||
? Array.isArray(value)
|
||||
? value.map((v) => ({ [idKey]: v }))
|
||||
: [{ [idKey]: value }]
|
||||
: []),
|
||||
],
|
||||
or: [...(searchPayload.where.or || [])],
|
||||
},
|
||||
};
|
||||
const { data, isFetching, fetchNextPage } = useInfiniteFind(
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { Avatar, Box, FormHelperText, FormLabel } from "@mui/material";
|
||||
import { forwardRef, useState } from "react";
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import { Button, CircularProgress } from "@mui/material";
|
||||
import { ChangeEvent, forwardRef } from "react";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Box, Input, styled } from "@mui/material";
|
||||
import { Box, CircularProgress, Input, styled } from "@mui/material";
|
||||
import randomSeed from "random-seed";
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
@@ -62,6 +62,7 @@ type SelectableProps = {
|
||||
text: string;
|
||||
entities: INlpDatasetKeywordEntity[];
|
||||
}) => void;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
const Selectable: FC<SelectableProps> = ({
|
||||
@@ -70,6 +71,7 @@ const Selectable: FC<SelectableProps> = ({
|
||||
placeholder = "",
|
||||
onChange,
|
||||
onSelect,
|
||||
loading = false,
|
||||
}) => {
|
||||
const [text, setText] = useState(defaultValue || "");
|
||||
const editableRef = useRef<HTMLDivElement>(null);
|
||||
@@ -204,6 +206,22 @@ const Selectable: FC<SelectableProps> = ({
|
||||
value={text}
|
||||
onChange={(e) => handleTextChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
endAdornment={
|
||||
loading ? (
|
||||
<CircularProgress
|
||||
size={20}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: "20%",
|
||||
transform: "translateY(-20%)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
aria-label="Loading..."
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</SelectableBox>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { Grid } from "@mui/material";
|
||||
import { GridRenderCellParams } from "@mui/x-data-grid";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MenuItem } from "@mui/material";
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { ToggleableInput } from "@/app-components/inputs/ToggleableInput";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Button } from "@mui/material";
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import { MenuItem } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "react-hook-form";
|
||||
|
||||
import AttachmentInput from "@/app-components/attachment/AttachmentInput";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Adornment } from "@/app-components/inputs/Adornment";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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 { FC, Fragment, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import AttachmentInput from "@/app-components/attachment/AttachmentInput";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { useApiClient } from "@/hooks/useApiClient";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { AttachmentResourceRef } from "@/types/attachment.types";
|
||||
import { ComponentFormProps } from "@/types/common/dialogs.types";
|
||||
import { IContentType } from "@/types/content-type.types";
|
||||
|
||||
export type ContentImportFormData = { row: null; contentType: IContentType };
|
||||
export const ContentImportForm: FC<
|
||||
ComponentFormProps<ContentImportFormData>
|
||||
> = ({ data, Wrapper = Fragment, WrapperProps, ...rest }) => {
|
||||
const [attachmentId, setAttachmentId] = useState<string | null>(null);
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { apiClient } = useApiClient();
|
||||
const { refetch, isFetching } = useQuery(
|
||||
["importContent", data?.contentType.id, attachmentId],
|
||||
async () => {
|
||||
if (data?.contentType.id && attachmentId) {
|
||||
await apiClient.importContent(data.contentType.id, attachmentId);
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
onError: () => {
|
||||
rest.onError?.();
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
rest.onSuccess?.();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
},
|
||||
);
|
||||
const handleImportClick = () => {
|
||||
if (attachmentId && data?.contentType.id) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
onSubmit={handleImportClick}
|
||||
{...WrapperProps}
|
||||
confirmButtonProps={{
|
||||
...WrapperProps?.confirmButtonProps,
|
||||
disabled: !attachmentId || isFetching,
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleImportClick}>
|
||||
<ContentContainer>
|
||||
<ContentItem>
|
||||
<AttachmentInput
|
||||
format="basic"
|
||||
accept="text/csv"
|
||||
onChange={(id, _) => {
|
||||
setAttachmentId(id);
|
||||
}}
|
||||
label=""
|
||||
value={attachmentId}
|
||||
resourceRef={AttachmentResourceRef.ContentAttachment}
|
||||
/>
|
||||
</ContentItem>
|
||||
</ContentContainer>
|
||||
</form>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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 { GenericFormDialog } from "@/app-components/dialogs";
|
||||
import { ComponentFormDialogProps } from "@/types/common/dialogs.types";
|
||||
|
||||
import { ContentImportForm, ContentImportFormData } from "./ContentImportForm";
|
||||
|
||||
export const ContentImportFormDialog = <
|
||||
T extends ContentImportFormData = ContentImportFormData,
|
||||
>(
|
||||
props: ComponentFormDialogProps<T>,
|
||||
) => (
|
||||
<GenericFormDialog<T>
|
||||
Form={ContentImportForm}
|
||||
rowKey="row"
|
||||
addText="button.import"
|
||||
confirmButtonProps={{ value: "button.import" }}
|
||||
{...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,12 +9,13 @@
|
||||
import { faAlignLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import { Button, Chip, Grid, Paper, Switch, Typography } from "@mui/material";
|
||||
import { Button, ButtonGroup, Chip, Grid, Paper, Switch, Typography } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useQueryClient } from "react-query";
|
||||
|
||||
import { ConfirmDialogBody } from "@/app-components/dialogs";
|
||||
import FileUploadButton from "@/app-components/inputs/FileInput";
|
||||
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||
import {
|
||||
ActionColumnLabel,
|
||||
@@ -22,9 +23,11 @@ import {
|
||||
} from "@/app-components/tables/columns/getColumns";
|
||||
import { renderHeader } from "@/app-components/tables/columns/renderHeader";
|
||||
import { DataGrid } from "@/app-components/tables/DataGrid";
|
||||
import { isSameEntity } from "@/hooks/crud/helpers";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useGet, useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useImport } from "@/hooks/crud/useImport";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { useDialogs } from "@/hooks/useDialogs";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
@@ -38,12 +41,12 @@ import { PermissionAction } from "@/types/permission.types";
|
||||
import { getDateTimeFormatter } from "@/utils/date";
|
||||
|
||||
import { ContentFormDialog } from "./ContentFormDialog";
|
||||
import { ContentImportFormDialog } from "./ContentImportFormDialog";
|
||||
|
||||
export const Contents = () => {
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { query } = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const dialogs = useDialogs();
|
||||
// data fetching
|
||||
const { onSearch, searchPayload } = useSearch<IContent>({
|
||||
@@ -51,7 +54,7 @@ export const Contents = () => {
|
||||
$iLike: ["title"],
|
||||
});
|
||||
const hasPermission = useHasPermission();
|
||||
const { dataGridProps, refetch } = useFind(
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CONTENT, format: Format.FULL },
|
||||
{
|
||||
params: searchPayload,
|
||||
@@ -97,8 +100,36 @@ export const Contents = () => {
|
||||
const { data: contentType } = useGet(String(query.id), {
|
||||
entity: EntityType.CONTENT_TYPE,
|
||||
});
|
||||
const { mutate: importDataset, isLoading } = useImport(
|
||||
EntityType.CONTENT,
|
||||
{
|
||||
onError: () => {
|
||||
toast.error(t("message.import_failed"));
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.removeQueries({
|
||||
predicate: ({ queryKey }) => {
|
||||
const [_qType, qEntity] = queryKey;
|
||||
|
||||
return (
|
||||
return (
|
||||
isSameEntity(qEntity, EntityType.CONTENT)
|
||||
);
|
||||
},
|
||||
});
|
||||
if (data.length) {
|
||||
toast.success(t("message.success_import"));
|
||||
} else {
|
||||
toast.error(t("message.import_duplicated_data"));
|
||||
}
|
||||
},
|
||||
},
|
||||
{ idTargetContentType: contentType?.id }
|
||||
);
|
||||
const handleImportChange = (file: File) => {
|
||||
importDataset(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container flexDirection="column" gap={3}>
|
||||
<Grid item height="fit-content" container>
|
||||
<Link href="/content/types">
|
||||
@@ -119,6 +150,7 @@ export const Contents = () => {
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? (
|
||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
@@ -127,30 +159,19 @@ export const Contents = () => {
|
||||
dialogs.open(ContentFormDialog, { contentType })
|
||||
}
|
||||
sx={{ float: "right" }}
|
||||
>
|
||||
{t("button.add")}
|
||||
</Button>
|
||||
</Grid>
|
||||
) : null}
|
||||
{hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<UploadIcon />}
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
if (contentType) {
|
||||
await dialogs.open(ContentImportFormDialog, {
|
||||
row: null,
|
||||
contentType,
|
||||
});
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
sx={{ float: "right" }}
|
||||
>
|
||||
{t("button.import")}
|
||||
</Button>
|
||||
</Grid>
|
||||
>
|
||||
{t("button.add")}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FileUploadButton
|
||||
accept="text/csv"
|
||||
label={t("button.import")}
|
||||
onChange={handleImportChange}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Grid>
|
||||
</ButtonGroup>
|
||||
) : null}
|
||||
</Grid>
|
||||
</PageHeader>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { FormControlLabel, FormHelperText, Switch } from "@mui/material";
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
@@ -39,11 +39,11 @@ export const ContextVarForm: FC<ComponentFormProps<IContextVar>> = ({
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
};
|
||||
const { mutateAsync: createContextVar } = useCreate(
|
||||
const { mutate: createContextVar } = useCreate(
|
||||
EntityType.CONTEXT_VAR,
|
||||
options,
|
||||
);
|
||||
const { mutateAsync: updateContextVar } = useUpdate(
|
||||
const { mutate: updateContextVar } = useUpdate(
|
||||
EntityType.CONTEXT_VAR,
|
||||
options,
|
||||
);
|
||||
@@ -72,7 +72,7 @@ export const ContextVarForm: FC<ComponentFormProps<IContextVar>> = ({
|
||||
required: t("message.label_is_required"),
|
||||
},
|
||||
};
|
||||
const onSubmitForm = async (params: IContextVarAttributes) => {
|
||||
const onSubmitForm = (params: IContextVarAttributes) => {
|
||||
if (data) {
|
||||
updateContextVar({ id: data.id, params });
|
||||
} else {
|
||||
|
||||
@@ -23,11 +23,11 @@ export const AttachmentViewerForm: FC<
|
||||
<Wrapper {...WrapperProps}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
width="auto"
|
||||
height={800}
|
||||
width="100%"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
objectFit: "contain",
|
||||
maxHeight: "70vh",
|
||||
}}
|
||||
alt={data?.url}
|
||||
src={data?.url}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import {
|
||||
@@ -30,7 +29,7 @@ import {
|
||||
|
||||
const CARD_WIDTH = 300;
|
||||
const StyledIconButton = styled(IconButton)({
|
||||
opacity: 0.1,
|
||||
opacity: 0.2,
|
||||
zIndex: 9999,
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
@@ -38,7 +37,9 @@ const StyledIconButton = styled(IconButton)({
|
||||
transition: "all 0.1s",
|
||||
".carousel-wrapper:hover &": {
|
||||
opacity: 1,
|
||||
backgroundColor: "#fff"
|
||||
},
|
||||
backgroundColor: "#fff"
|
||||
});
|
||||
const StyledCarouselDiv = styled("div")({
|
||||
display: "flex",
|
||||
|
||||
@@ -39,7 +39,7 @@ export function Chat() {
|
||||
const { t, i18n } = useTranslate();
|
||||
const { subscriber } = useChat();
|
||||
const { user } = useAuth();
|
||||
const { mutateAsync: createMessage } = useCreate(EntityType.MESSAGE);
|
||||
const { mutate: createMessage } = useCreate(EntityType.MESSAGE);
|
||||
const { replyTo, messages, fetchNextPage, hasNextPage, isFetching } =
|
||||
useInfinitedLiveMessages();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Conversation,
|
||||
@@ -14,11 +13,13 @@ import {
|
||||
} from "@chatscope/chat-ui-kit-react";
|
||||
import InboxIcon from "@mui/icons-material/MoveToInbox";
|
||||
import { Chip, debounce, Grid } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { Title } from "@/layout/content/Title";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { EntityType, RouterType } from "@/services/types";
|
||||
|
||||
import { getAvatarSrc } from "../helpers/mapMessages";
|
||||
import { useChat } from "../hooks/ChatContext";
|
||||
@@ -30,6 +31,8 @@ export const SubscribersList = (props: {
|
||||
searchPayload: any;
|
||||
assignedTo: AssignedTo;
|
||||
}) => {
|
||||
const { query, push } = useRouter();
|
||||
const subscriber = query.subscriber?.toString() || null;
|
||||
const { apiUrl } = useConfig();
|
||||
const { t, i18n } = useTranslate();
|
||||
const chat = useChat();
|
||||
@@ -39,6 +42,12 @@ export const SubscribersList = (props: {
|
||||
!isFetching && hasNextPage && fetchNextPage();
|
||||
}, 400);
|
||||
|
||||
useEffect(() => {
|
||||
if (chat) {
|
||||
chat.setSubscriberId(subscriber);
|
||||
}
|
||||
}, [chat, subscriber]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid padding={2}>
|
||||
@@ -53,7 +62,10 @@ export const SubscribersList = (props: {
|
||||
>
|
||||
{subscribers.map((subscriber) => (
|
||||
<Conversation
|
||||
onClick={() => chat.setSubscriberId(subscriber.id)}
|
||||
onClick={() => {
|
||||
chat.setSubscriberId(subscriber.id);
|
||||
push(`/${RouterType.INBOX}/subscribers/${subscriber.id}`);
|
||||
}}
|
||||
className="changeColor"
|
||||
key={subscriber.id}
|
||||
active={chat.subscriber?.id === subscriber.id}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
* 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 { faTags } from "@fortawesome/free-solid-svg-icons";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { Button, Grid, Paper } from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ConfirmDialogBody } from "@/app-components/dialogs";
|
||||
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import { renderHeader } from "@/app-components/tables/columns/renderHeader";
|
||||
import { DataGrid } from "@/app-components/tables/DataGrid";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useDialogs } from "@/hooks/useDialogs";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
@@ -49,14 +51,16 @@ export const Labels = () => {
|
||||
params: searchPayload,
|
||||
},
|
||||
);
|
||||
const { mutate: deleteLabel } = useDelete(EntityType.LABEL, {
|
||||
const options = {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess() {
|
||||
toast.success(t("message.item_delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
const { mutate: deleteLabel } = useDelete(EntityType.LABEL, options);
|
||||
const { mutate: deleteLabels } = useDeleteMany(EntityType.LABEL, options);
|
||||
const actionColumns = useActionColumns<ILabel>(
|
||||
EntityType.LABEL,
|
||||
[
|
||||
@@ -79,6 +83,7 @@ export const Labels = () => {
|
||||
],
|
||||
t("label.operations"),
|
||||
);
|
||||
const [selectedLabels, setSelectedLabels] = useState<string[]>([]);
|
||||
const columns: GridColDef<ILabel>[] = [
|
||||
{ field: "id", headerName: "ID" },
|
||||
{
|
||||
@@ -124,7 +129,6 @@ export const Labels = () => {
|
||||
|
||||
headerAlign: "left",
|
||||
},
|
||||
|
||||
{
|
||||
minWidth: 140,
|
||||
field: "createdAt",
|
||||
@@ -149,6 +153,9 @@ export const Labels = () => {
|
||||
},
|
||||
actionColumns,
|
||||
];
|
||||
const handleSelectionChange = (selection: GridRowSelectionModel) => {
|
||||
setSelectedLabels(selection as string[]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container gap={3} flexDirection="column">
|
||||
@@ -176,12 +183,35 @@ export const Labels = () => {
|
||||
</Button>
|
||||
</Grid>
|
||||
) : null}
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
const isConfirmed = await dialogs.confirm(ConfirmDialogBody, {
|
||||
mode: "selection",
|
||||
count: selectedLabels.length,
|
||||
});
|
||||
|
||||
if (isConfirmed) {
|
||||
deleteLabels(selectedLabels);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedLabels.length}
|
||||
startIcon={<DeleteIcon />}
|
||||
>
|
||||
{t("button.delete")}
|
||||
</Button>
|
||||
</Grid>
|
||||
</PageHeader>
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ padding: 2 }}>
|
||||
<Grid>
|
||||
<DataGrid columns={columns} {...dataGridProps} />
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
{...dataGridProps}
|
||||
checkboxSelection
|
||||
onRowSelectionModelChange={handleSelectionChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { FormControlLabel, Switch } from "@mui/material";
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -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.
|
||||
@@ -52,7 +52,7 @@ export const Languages = () => {
|
||||
params: searchPayload,
|
||||
},
|
||||
);
|
||||
const { mutateAsync: updateLanguage } = useUpdate(EntityType.LANGUAGE, {
|
||||
const { mutate: updateLanguage } = useUpdate(EntityType.LANGUAGE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
@@ -61,7 +61,7 @@ export const Languages = () => {
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const { mutateAsync: deleteLanguage } = useDelete(EntityType.LANGUAGE, {
|
||||
const { mutate: deleteLanguage } = useDelete(EntityType.LANGUAGE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import DriveFolderUploadIcon from "@mui/icons-material/DriveFolderUpload";
|
||||
import { Box, Grid, Paper } from "@mui/material";
|
||||
import { GridColDef, GridEventListener } from "@mui/x-data-grid";
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
@@ -111,7 +110,7 @@ export default function NlpSample() {
|
||||
toast.success(t("message.item_delete_success"));
|
||||
},
|
||||
});
|
||||
const { mutateAsync: importDataset, isLoading } = useImport(
|
||||
const { mutate: importDataset, isLoading } = useImport(
|
||||
EntityType.NLP_SAMPLE,
|
||||
{
|
||||
onError: () => {
|
||||
@@ -298,8 +297,8 @@ export default function NlpSample() {
|
||||
const handleSelectionChange = (selection: GridRowSelectionModel) => {
|
||||
setSelectedNlpSamples(selection as string[]);
|
||||
};
|
||||
const handleImportChange = async (file: File) => {
|
||||
await importDataset(file);
|
||||
const handleImportChange = (file: File) => {
|
||||
importDataset(file);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -397,26 +396,24 @@ export default function NlpSample() {
|
||||
{t("button.export")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<DeleteIcon />}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={async () => {
|
||||
const isConfirmed = await dialogs.confirm(ConfirmDialogBody, {
|
||||
mode: "selection",
|
||||
count: selectedNlpSamples.length,
|
||||
});
|
||||
<Button
|
||||
startIcon={<DeleteIcon />}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={async () => {
|
||||
const isConfirmed = await dialogs.confirm(ConfirmDialogBody, {
|
||||
mode: "selection",
|
||||
count: selectedNlpSamples.length,
|
||||
});
|
||||
|
||||
if (isConfirmed) {
|
||||
deleteNlpSamples(selectedNlpSamples);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedNlpSamples.length}
|
||||
>
|
||||
{t("button.delete")}
|
||||
</Button>
|
||||
</Grid>
|
||||
if (isConfirmed) {
|
||||
deleteNlpSamples(selectedNlpSamples);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedNlpSamples.length}
|
||||
>
|
||||
{t("button.delete")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
</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.
|
||||
@@ -117,8 +117,7 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
}, 400),
|
||||
[setValue],
|
||||
);
|
||||
|
||||
useQuery({
|
||||
const { isLoading } = useQuery({
|
||||
queryKey: ["nlp-prediction", currentText],
|
||||
queryFn: async () => {
|
||||
return await apiClient.predictNlp(currentText);
|
||||
@@ -140,7 +139,6 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
},
|
||||
enabled: !sample && !!currentText,
|
||||
});
|
||||
|
||||
const findInsertIndex = (newItem: INlpDatasetKeywordEntity): number => {
|
||||
const index = keywordEntities.findIndex(
|
||||
(entity) => entity.start && newItem.start && entity.start > newItem.start,
|
||||
@@ -226,6 +224,7 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
})),
|
||||
);
|
||||
}}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</ContentItem>
|
||||
<Box display="flex" flexDirection="column">
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useRouter } from "next/router";
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import MultipleInput from "@/app-components/inputs/MultipleInput";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { faGraduationCap } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Grid, Paper, Tab, Tabs } from "@mui/material";
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -63,7 +62,7 @@ export const Nlp = ({
|
||||
};
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: createSample } = useCreate<
|
||||
const { mutate: createSample } = useCreate<
|
||||
EntityType.NLP_SAMPLE,
|
||||
INlpDatasetSampleAttributes,
|
||||
INlpSample,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import KeyIcon from "@mui/icons-material/Key";
|
||||
@@ -16,8 +15,7 @@ import { FC } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useQueryClient } from "react-query";
|
||||
|
||||
import { ContentItem } from "@/app-components/dialogs";
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Adornment } from "@/app-components/inputs/Adornment";
|
||||
import AvatarInput from "@/app-components/inputs/AvatarInput";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
@@ -36,7 +34,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
||||
const { t } = useTranslate();
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: updateProfile, isLoading } = useUpdateProfile({
|
||||
const { mutate: updateProfile, isLoading } = useUpdateProfile({
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
@@ -81,12 +79,12 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const onSubmitForm = async ({
|
||||
const onSubmitForm = ({
|
||||
password,
|
||||
password2: _password2,
|
||||
...rest
|
||||
}: IProfileAttributes) => {
|
||||
await updateProfile({
|
||||
updateProfile({
|
||||
...rest,
|
||||
password: password || undefined,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import KeyIcon from "@mui/icons-material/Key";
|
||||
import { FormControlLabel, MenuItem, Switch } from "@mui/material";
|
||||
import { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
@@ -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.
|
||||
@@ -85,7 +85,7 @@ export const Settings = () => {
|
||||
initialSortState: [{ field: "weight", sort: "asc" }],
|
||||
},
|
||||
);
|
||||
const { mutateAsync: updateSetting } = useUpdate(EntityType.SETTING, {
|
||||
const { mutate: updateSetting } = useUpdate(EntityType.SETTING, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, Grid, Link } from "@mui/material";
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, Grid, Link } from "@mui/material";
|
||||
import { FC, Fragment, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useSendInvitation } from "@/hooks/entities/invitation-hooks";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { FormControlLabel, Grid, Switch, Tab, Tabs } from "@mui/material";
|
||||
import { FC, Fragment, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import TriggerIcon from "@/app-components/svg/TriggerIcon";
|
||||
import { TabPanel } from "@/app-components/tabs/TabPanel";
|
||||
@@ -46,7 +46,7 @@ export const BlockEditForm: FC<ComponentFormProps<IBlock>> = ({
|
||||
setSelectedTab(newValue);
|
||||
};
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, {
|
||||
const { mutate: updateBlock } = useUpdate(EntityType.BLOCK, {
|
||||
onError: () => {
|
||||
rest.onError?.();
|
||||
toast.error(t("message.internal_server_error"));
|
||||
@@ -59,6 +59,7 @@ export const BlockEditForm: FC<ComponentFormProps<IBlock>> = ({
|
||||
const DEFAULT_VALUES = {
|
||||
name: block?.name || "",
|
||||
patterns: block?.patterns || [],
|
||||
outcomes: block?.outcomes || [],
|
||||
trigger_labels: block?.trigger_labels || [],
|
||||
trigger_channels: block?.trigger_channels || [],
|
||||
options: block?.options || {
|
||||
@@ -94,7 +95,7 @@ export const BlockEditForm: FC<ComponentFormProps<IBlock>> = ({
|
||||
required: t("message.name_is_required"),
|
||||
},
|
||||
};
|
||||
const onSubmitForm = async (params: IBlockAttributes) => {
|
||||
const onSubmitForm = (params: IBlockAttributes) => {
|
||||
if (block) {
|
||||
updateBlock({ id: block.id, params });
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { MenuItem, Select } from "@mui/material";
|
||||
import { FC, Fragment, useState } from "react";
|
||||
|
||||
import { ContentContainer } from "@/app-components/dialogs/";
|
||||
import { ContentContainer } from "@/app-components/dialogs";
|
||||
import { ICategory } from "@/types/category.types";
|
||||
import { ComponentFormProps } from "@/types/common/dialogs.types";
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { IBlockAttributes } from "@/types/block.types";
|
||||
import {
|
||||
ButtonType,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import AttachmentInput from "@/app-components/attachment/AttachmentInput";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentContainer } from "@/app-components/dialogs";
|
||||
|
||||
import AttachmentMessageForm from "./AttachmentMessageForm";
|
||||
import { useBlock } from "./BlockFormProvider";
|
||||
|
||||
@@ -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,8 +9,7 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
@@ -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.
|
||||
@@ -16,6 +16,7 @@ import { useFind } from "@/hooks/crud/useFind";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { IBlockAttributes } from "@/types/block.types";
|
||||
import { StdPluginMessage } from "@/types/message.types";
|
||||
import { getNamespace } from "@/utils/string";
|
||||
|
||||
import { useBlock } from "./BlockFormProvider";
|
||||
|
||||
@@ -63,8 +64,7 @@ const PluginMessageForm = () => {
|
||||
<SettingInput
|
||||
setting={setting}
|
||||
field={field}
|
||||
// @TODO : clean this later
|
||||
ns={message.plugin.replaceAll("-", "_")}
|
||||
ns={getNamespace(message.plugin)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -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,8 +9,7 @@
|
||||
import { Divider } from "@mui/material";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user