mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix(frontend): resolve file conflicts
This commit is contained in:
17
api/package-lock.json
generated
17
api/package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"@tekuconcept/nestjs-csrf": "^1.1.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager": "^5.3.2",
|
||||
"cache-manager-redis-yet": "^4.1.2",
|
||||
@@ -7411,10 +7412,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz",
|
||||
"integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==",
|
||||
"dev": true,
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
|
||||
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@@ -15003,6 +15003,15 @@
|
||||
"node": ">=14.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-memory-server-core/node_modules/async-mutex": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz",
|
||||
"integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-memory-server-core/node_modules/bson": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"@tekuconcept/nestjs-csrf": "^1.1.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager": "^5.3.2",
|
||||
"cache-manager-redis-yet": "^4.1.2",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
|
||||
export class AppInstance {
|
||||
private static app: INestApplication;
|
||||
private static app: INestApplication | null = null;
|
||||
|
||||
static setApp(app: INestApplication) {
|
||||
this.app = app;
|
||||
@@ -21,4 +21,13 @@ export class AppInstance {
|
||||
}
|
||||
return this.app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the application context is initialized.
|
||||
* This may return `false` in environments where the app instance is not set,
|
||||
* such as when running in test env or CLI mode without a full application bootstrap.
|
||||
*/
|
||||
static isReady(): boolean {
|
||||
return this.app !== null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Module, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
|
||||
import { AppInstance } from '@/app.instance';
|
||||
import { config } from '@/config';
|
||||
import { UserModule } from '@/user/user.module';
|
||||
|
||||
@@ -34,6 +35,10 @@ import { AttachmentService } from './services/attachment.service';
|
||||
})
|
||||
export class AttachmentModule implements OnApplicationBootstrap {
|
||||
onApplicationBootstrap() {
|
||||
if (!AppInstance.isReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the directories exists
|
||||
if (!existsSync(config.parameters.uploadDir)) {
|
||||
mkdirSync(config.parameters.uploadDir, { recursive: true });
|
||||
|
||||
@@ -120,13 +120,13 @@ export class ChannelService {
|
||||
*/
|
||||
@SocketGet(`/webhook/${WEB_CHANNEL_NAME}/`)
|
||||
@SocketPost(`/webhook/${WEB_CHANNEL_NAME}/`)
|
||||
handleWebsocketForWebChannel(
|
||||
async handleWebsocketForWebChannel(
|
||||
@SocketReq() req: SocketRequest,
|
||||
@SocketRes() res: SocketResponse,
|
||||
) {
|
||||
this.logger.log('Channel notification (Web Socket) : ', req.method);
|
||||
const handler = this.getChannelHandler(WEB_CHANNEL_NAME);
|
||||
return handler.handle(req, res);
|
||||
return await handler.handle(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,6 +195,6 @@ export class ChannelService {
|
||||
}
|
||||
|
||||
const handler = this.getChannelHandler(CONSOLE_CHANNEL_NAME);
|
||||
return handler.handle(req, res);
|
||||
return await handler.handle(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { NlpValueService } from '@/nlp/services/nlp-value.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { FALLBACK_DEFAULT_NLU_PENALTY_FACTOR } from '@/utils/constants/nlp';
|
||||
import {
|
||||
blockFixtures,
|
||||
installBlockFixtures,
|
||||
@@ -196,6 +197,7 @@ describe('BlockService', () => {
|
||||
})),
|
||||
getSettings: jest.fn(() => ({
|
||||
contact: { company_name: 'Your company name' },
|
||||
chatbot_settings: { default_nlu_penalty_factor: 0.95 },
|
||||
})),
|
||||
},
|
||||
},
|
||||
@@ -467,9 +469,11 @@ describe('BlockService', () => {
|
||||
blockService,
|
||||
'calculateNluPatternMatchScore',
|
||||
);
|
||||
|
||||
const bestBlock = blockService.matchBestNLP(
|
||||
blocks,
|
||||
mockNlpGreetingNameEntities,
|
||||
FALLBACK_DEFAULT_NLU_PENALTY_FACTOR,
|
||||
);
|
||||
|
||||
// Ensure calculateBlockScore was called at least once for each block
|
||||
@@ -509,7 +513,11 @@ describe('BlockService', () => {
|
||||
blockService,
|
||||
'calculateNluPatternMatchScore',
|
||||
);
|
||||
const bestBlock = blockService.matchBestNLP(blocks, nlp);
|
||||
const bestBlock = blockService.matchBestNLP(
|
||||
blocks,
|
||||
nlp,
|
||||
FALLBACK_DEFAULT_NLU_PENALTY_FACTOR,
|
||||
);
|
||||
|
||||
// Ensure calculateBlockScore was called at least once for each block
|
||||
expect(calculateBlockScoreSpy).toHaveBeenCalledTimes(3); // Called for each block
|
||||
@@ -530,6 +538,7 @@ describe('BlockService', () => {
|
||||
const bestBlock = blockService.matchBestNLP(
|
||||
blocks,
|
||||
mockNlpGreetingNameEntities,
|
||||
FALLBACK_DEFAULT_NLU_PENALTY_FACTOR,
|
||||
);
|
||||
|
||||
// Assert that undefined is returned when no blocks are available
|
||||
@@ -542,6 +551,7 @@ describe('BlockService', () => {
|
||||
const matchingScore = blockService.calculateNluPatternMatchScore(
|
||||
mockNlpGreetingNamePatterns,
|
||||
mockNlpGreetingNameEntities,
|
||||
FALLBACK_DEFAULT_NLU_PENALTY_FACTOR,
|
||||
);
|
||||
|
||||
expect(matchingScore).toBeGreaterThan(0);
|
||||
@@ -551,15 +561,29 @@ describe('BlockService', () => {
|
||||
const scoreWithoutPenalty = blockService.calculateNluPatternMatchScore(
|
||||
mockNlpGreetingNamePatterns,
|
||||
mockNlpGreetingNameEntities,
|
||||
FALLBACK_DEFAULT_NLU_PENALTY_FACTOR,
|
||||
);
|
||||
|
||||
const scoreWithPenalty = blockService.calculateNluPatternMatchScore(
|
||||
mockNlpGreetingAnyNamePatterns,
|
||||
mockNlpGreetingNameEntities,
|
||||
FALLBACK_DEFAULT_NLU_PENALTY_FACTOR,
|
||||
);
|
||||
|
||||
expect(scoreWithoutPenalty).toBeGreaterThan(scoreWithPenalty);
|
||||
});
|
||||
|
||||
it('should handle invalid case for penalty factor values', async () => {
|
||||
// Test with invalid penalty (should use fallback)
|
||||
const scoreWithInvalidPenalty =
|
||||
blockService.calculateNluPatternMatchScore(
|
||||
mockNlpGreetingAnyNamePatterns,
|
||||
mockNlpGreetingNameEntities,
|
||||
-1,
|
||||
);
|
||||
|
||||
expect(scoreWithInvalidPenalty).toBeGreaterThan(0); // Should use fallback value
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchPayload', () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { PluginType } from '@/plugins/types';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { FALLBACK_DEFAULT_NLU_PENALTY_FACTOR } from '@/utils/constants/nlp';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import { getRandomElement } from '@/utils/helpers/safeRandom';
|
||||
|
||||
@@ -180,8 +181,23 @@ export class BlockService extends BaseService<
|
||||
const scoredEntities =
|
||||
await this.nlpService.computePredictionScore(nlp);
|
||||
|
||||
const settings = await this.settingService.getSettings();
|
||||
let penaltyFactor =
|
||||
settings.chatbot_settings?.default_nlu_penalty_factor;
|
||||
if (!penaltyFactor) {
|
||||
this.logger.warn(
|
||||
'Using fallback NLU penalty factor value: %s',
|
||||
FALLBACK_DEFAULT_NLU_PENALTY_FACTOR,
|
||||
);
|
||||
penaltyFactor = FALLBACK_DEFAULT_NLU_PENALTY_FACTOR;
|
||||
}
|
||||
|
||||
if (scoredEntities.entities.length > 0) {
|
||||
block = this.matchBestNLP(filteredBlocks, scoredEntities);
|
||||
block = this.matchBestNLP(
|
||||
filteredBlocks,
|
||||
scoredEntities,
|
||||
penaltyFactor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,6 +367,7 @@ export class BlockService extends BaseService<
|
||||
matchBestNLP<B extends BlockStub>(
|
||||
blocks: B[],
|
||||
scoredEntities: NLU.ScoredEntities,
|
||||
penaltyFactor: number,
|
||||
): B | undefined {
|
||||
const bestMatch = blocks.reduce(
|
||||
(bestMatch, block) => {
|
||||
@@ -365,10 +382,10 @@ export class BlockService extends BaseService<
|
||||
const score = this.calculateNluPatternMatchScore(
|
||||
patterns,
|
||||
scoredEntities,
|
||||
penaltyFactor,
|
||||
);
|
||||
return Math.max(maxScore, score);
|
||||
}, 0);
|
||||
|
||||
return score > bestMatch.score ? { block, score } : bestMatch;
|
||||
},
|
||||
{ block: undefined, score: 0 },
|
||||
@@ -390,14 +407,13 @@ export class BlockService extends BaseService<
|
||||
*
|
||||
* @param patterns - A list of patterns to evaluate against the NLU prediction.
|
||||
* @param prediction - The scored entities resulting from NLU inference.
|
||||
* @param [penaltyFactor=0.95] - Optional penalty factor to apply for generic matches (default is 0.95).
|
||||
*
|
||||
* @returns The total aggregated match score based on matched patterns and their computed scores.
|
||||
*/
|
||||
calculateNluPatternMatchScore(
|
||||
patterns: NlpPattern[],
|
||||
prediction: NLU.ScoredEntities,
|
||||
penaltyFactor = 0.95,
|
||||
penaltyFactor: number,
|
||||
): number {
|
||||
if (!patterns.length || !prediction.entities.length) {
|
||||
return 0;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { Global, Module, OnApplicationBootstrap } from '@nestjs/common';
|
||||
|
||||
import { AppInstance } from '@/app.instance';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { CleanupService } from './cleanup.service';
|
||||
@@ -24,6 +25,11 @@ export class ExtensionModule implements OnApplicationBootstrap {
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
if (!AppInstance.isReady()) {
|
||||
// bypass in test or CLI env
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.cleanupService.pruneExtensionSettings();
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import Handlebars from 'handlebars';
|
||||
|
||||
import { AppInstance } from '@/app.instance';
|
||||
import { HelperService } from '@/helper/helper.service';
|
||||
import BaseNlpHelper from '@/helper/lib/base-nlp-helper';
|
||||
import { HelperType, LLM, NLU } from '@/helper/types';
|
||||
@@ -24,7 +25,7 @@ import { LLM_NLU_HELPER_NAME } from './settings';
|
||||
@Injectable()
|
||||
export default class LlmNluHelper
|
||||
extends BaseNlpHelper<typeof LLM_NLU_HELPER_NAME>
|
||||
implements OnModuleInit
|
||||
implements OnApplicationBootstrap
|
||||
{
|
||||
private languageClassifierPrompt: string;
|
||||
|
||||
@@ -50,13 +51,18 @@ export default class LlmNluHelper
|
||||
@OnEvent('hook:language:*')
|
||||
@OnEvent('hook:llm_nlu_helper:language_classifier_prompt_template')
|
||||
async buildLanguageClassifierPrompt() {
|
||||
const settings = await this.getSettings();
|
||||
if (settings) {
|
||||
try {
|
||||
const settings = await this.getSettings();
|
||||
const languages = await this.languageService.findAll();
|
||||
const delegate = Handlebars.compile(
|
||||
settings.language_classifier_prompt_template,
|
||||
);
|
||||
this.languageClassifierPrompt = delegate({ languages });
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
'Settings for LLM NLU helper not found or invalid, language classifier prompt will not be built.',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +70,8 @@ export default class LlmNluHelper
|
||||
@OnEvent('hook:nlpValue:*')
|
||||
@OnEvent('hook:llm_nlu_helper:trait_classifier_prompt_template')
|
||||
async buildClassifiersPrompt() {
|
||||
const settings = await this.getSettings();
|
||||
if (settings) {
|
||||
try {
|
||||
const settings = await this.getSettings();
|
||||
const traitEntities = await this.nlpEntityService.findAndPopulate({
|
||||
lookups: 'trait',
|
||||
});
|
||||
@@ -75,14 +81,30 @@ export default class LlmNluHelper
|
||||
entity,
|
||||
}),
|
||||
}));
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
'Settings for LLM NLU helper not found or invalid, trait classifier prompts will not be built.',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
super.onModuleInit();
|
||||
async onApplicationBootstrap() {
|
||||
if (!AppInstance.isReady()) {
|
||||
// bypass in Test / CLI env
|
||||
return;
|
||||
}
|
||||
|
||||
await this.buildLanguageClassifierPrompt();
|
||||
await this.buildClassifiersPrompt();
|
||||
try {
|
||||
this.logger.log('Initializing LLM NLU helper, building prompts...');
|
||||
// Build prompts for language and trait classifiers
|
||||
// This is done on application bootstrap to ensure that the settings are loaded
|
||||
// and the prompts are built before any requests are made to the helper.
|
||||
await this.buildLanguageClassifierPrompt();
|
||||
await this.buildClassifiersPrompt();
|
||||
} catch (error) {
|
||||
this.logger.error('Unable to initialize LLM NLU helper', error);
|
||||
}
|
||||
}
|
||||
|
||||
async predict(text: string): Promise<NLU.ParseEntities> {
|
||||
|
||||
135
api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts
Normal file
135
api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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 mongoose from 'mongoose';
|
||||
|
||||
import blockSchema, { Block } from '@/chat/schemas/block.schema';
|
||||
import roleSchema, { Role } from '@/user/schemas/role.schema';
|
||||
import userSchema, { User } from '@/user/schemas/user.schema';
|
||||
|
||||
import { MigrationServices } from '../types';
|
||||
|
||||
/**
|
||||
* @returns The admin user or null
|
||||
*/
|
||||
const getAdminUser = async () => {
|
||||
const RoleModel = mongoose.model<Role>(Role.name, roleSchema);
|
||||
const UserModel = mongoose.model<User>(User.name, userSchema);
|
||||
|
||||
const adminRole = await RoleModel.findOne({ name: 'admin' });
|
||||
const user = await UserModel.findOne({ roles: [adminRole!._id] }).sort({
|
||||
createdAt: 'asc',
|
||||
});
|
||||
|
||||
return user!;
|
||||
};
|
||||
|
||||
const migrateBlockOptionsContentLimit = async (services: MigrationServices) => {
|
||||
const BlockModel = mongoose.model<Block>(Block.name, blockSchema).collection;
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
|
||||
if (!adminUser) {
|
||||
services.logger.warn('Unable to process block, no admin user found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await BlockModel.updateMany(
|
||||
{ 'options.content.limit': { $exists: true } },
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
'options.content.limit': { $toInt: '$options.content.limit' },
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
services.logger.error(`Failed to update limit : ${error.message}`);
|
||||
|
||||
throw error instanceof Error ? error : new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const migrateBlockOptionsContentButtonsUrl = async (
|
||||
services: MigrationServices,
|
||||
) => {
|
||||
const BlockModel = mongoose.model<Block>(Block.name, blockSchema).collection;
|
||||
|
||||
try {
|
||||
await BlockModel.updateMany(
|
||||
{ 'options.content.buttons.url': false },
|
||||
{
|
||||
$set: {
|
||||
'options.content.buttons.$[].url': '',
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
services.logger.error(`Failed to update button url : ${error.message}`);
|
||||
|
||||
throw error instanceof Error ? error : new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const migrateBlockOptionsFallback = async (services: MigrationServices) => {
|
||||
const BlockModel = mongoose.model<Block>(Block.name, blockSchema).collection;
|
||||
|
||||
try {
|
||||
await BlockModel.updateMany(
|
||||
{ 'options.fallback.max_attempts': { $exists: true, $type: 'string' } },
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
'options.fallback.max_attempts': {
|
||||
$toInt: '$options.fallback.max_attempts',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
services.logger.error(`Failed to update max_attempts : ${error.message}`);
|
||||
throw error instanceof Error ? error : new Error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await BlockModel.updateMany({ 'options.fallback.message': { $size: 0 } }, [
|
||||
{
|
||||
$set: {
|
||||
'options.fallback.max_attempts': 0,
|
||||
'options.fallback.active': false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
services.logger.error(
|
||||
`Failed to update max_attempts, active : ${error.message}`,
|
||||
);
|
||||
throw error instanceof Error ? error : new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async up(services: MigrationServices) {
|
||||
try {
|
||||
await migrateBlockOptionsContentLimit(services);
|
||||
await migrateBlockOptionsContentButtonsUrl(services);
|
||||
await migrateBlockOptionsFallback(services);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
services.logger.error(`Migration failed : ${error.message}`);
|
||||
throw error instanceof Error ? error : new Error(error);
|
||||
}
|
||||
},
|
||||
async down(_services: MigrationServices) {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -222,6 +222,7 @@ export class NlpEntityController extends BaseController<
|
||||
if (!ids?.length) {
|
||||
throw new BadRequestException('No IDs provided for deletion.');
|
||||
}
|
||||
|
||||
const deleteResult = await this.nlpEntityService.deleteMany({
|
||||
_id: { $in: ids },
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('NlpValueController', () => {
|
||||
entity: intentNlpEntity!.id,
|
||||
value: 'updated',
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
builtin: false,
|
||||
doc: '',
|
||||
};
|
||||
const result = await nlpValueController.updateOne(
|
||||
@@ -191,7 +191,6 @@ describe('NlpValueController', () => {
|
||||
describe('deleteMany', () => {
|
||||
it('should delete multiple nlp values', async () => {
|
||||
const valuesToDelete = [positiveValue!.id, negativeValue!.id];
|
||||
|
||||
const result = await nlpValueController.deleteMany(valuesToDelete);
|
||||
|
||||
expect(result.deletedCount).toEqual(valuesToDelete.length);
|
||||
|
||||
@@ -63,6 +63,14 @@ export class NlpEntityCreateDto {
|
||||
}
|
||||
|
||||
export class NlpEntityUpdateDto {
|
||||
@ApiPropertyOptional({ description: 'Name of the nlp entity', type: String })
|
||||
@Matches(/^[a-zA-Z0-9_]+$/, {
|
||||
message: 'Only alphanumeric characters and underscores are allowed.',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ type: String })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
@@ -232,7 +232,7 @@ describe('NlpEntityService', () => {
|
||||
{
|
||||
value: 'jhon',
|
||||
expressions: ['john', 'joohn', 'jhonny'],
|
||||
builtin: true,
|
||||
builtin: false,
|
||||
doc: '',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -24,6 +24,18 @@ export const DEFAULT_SETTINGS = [
|
||||
},
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_nlu_penalty_factor',
|
||||
value: 0.95,
|
||||
type: SettingType.number,
|
||||
config: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_llm_helper',
|
||||
@@ -36,7 +48,7 @@ export const DEFAULT_SETTINGS = [
|
||||
idKey: 'name',
|
||||
labelKey: 'name',
|
||||
},
|
||||
weight: 2,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
@@ -50,14 +62,14 @@ export const DEFAULT_SETTINGS = [
|
||||
idKey: 'name',
|
||||
labelKey: 'name',
|
||||
},
|
||||
weight: 3,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'global_fallback',
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 4,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
@@ -72,7 +84,7 @@ export const DEFAULT_SETTINGS = [
|
||||
idKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
weight: 5,
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
@@ -82,7 +94,7 @@ export const DEFAULT_SETTINGS = [
|
||||
"I'm really sorry but i don't quite understand what you are saying :(",
|
||||
] as string[],
|
||||
type: SettingType.multiple_text,
|
||||
weight: 6,
|
||||
weight: 7,
|
||||
translatable: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -138,6 +138,7 @@ describe('PermissionRepository', () => {
|
||||
|
||||
expect(permissionModel.deleteOne).toHaveBeenCalledWith({
|
||||
_id: permissionToDelete.id,
|
||||
builtin: { $ne: true },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
/*
|
||||
* 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 { ReactElement } from "react";
|
||||
|
||||
import { Inbox } from "@/components/inbox";
|
||||
import { Layout } from "@/layout";
|
||||
|
||||
const InboxPage = () => {
|
||||
return <Inbox />;
|
||||
};
|
||||
|
||||
InboxPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout hasNoPadding>{page}</Layout>;
|
||||
};
|
||||
|
||||
export default InboxPage;
|
||||
export const FALLBACK_DEFAULT_NLU_PENALTY_FACTOR = 0.95;
|
||||
@@ -306,6 +306,7 @@ describe('BaseRepository', () => {
|
||||
|
||||
expect(dummyModel.deleteOne).toHaveBeenCalledWith({
|
||||
_id: createdId,
|
||||
builtin: { $ne: true },
|
||||
});
|
||||
expect(result).toEqualPayload({ acknowledged: true, deletedCount: 1 });
|
||||
});
|
||||
@@ -318,12 +319,13 @@ describe('BaseRepository', () => {
|
||||
|
||||
expect(dummyModel.deleteOne).toHaveBeenCalledWith({
|
||||
dummy: 'dummy test 2',
|
||||
builtin: { $ne: true },
|
||||
});
|
||||
expect(result).toEqualPayload({ acknowledged: true, deletedCount: 1 });
|
||||
});
|
||||
|
||||
it('should call lifecycle hooks appropriately when deleting by id', async () => {
|
||||
const criteria = createdId;
|
||||
jest.spyOn(dummyModel, 'deleteOne');
|
||||
|
||||
// Spies for lifecycle hooks
|
||||
const spyBeforeDelete = jest
|
||||
@@ -333,7 +335,12 @@ describe('BaseRepository', () => {
|
||||
.spyOn(dummyRepository, 'postDelete')
|
||||
.mockResolvedValue();
|
||||
|
||||
await dummyRepository.deleteOne(criteria);
|
||||
await dummyRepository.deleteOne(createdId);
|
||||
|
||||
expect(dummyModel.deleteOne).toHaveBeenCalledWith({
|
||||
_id: createdId,
|
||||
builtin: { $ne: true },
|
||||
});
|
||||
|
||||
// Verifying that lifecycle hooks are called with correct parameters
|
||||
expect(spyBeforeDelete).toHaveBeenCalledTimes(1);
|
||||
@@ -341,6 +348,7 @@ describe('BaseRepository', () => {
|
||||
expect.objectContaining({ $useProjection: true }),
|
||||
{
|
||||
_id: new Types.ObjectId(createdId),
|
||||
builtin: { $ne: true },
|
||||
},
|
||||
);
|
||||
expect(spyAfterDelete).toHaveBeenCalledWith(
|
||||
|
||||
@@ -555,13 +555,15 @@ export abstract class BaseRepository<
|
||||
}
|
||||
|
||||
async deleteOne(criteria: string | TFilterQuery<T>): Promise<DeleteResult> {
|
||||
const filter = typeof criteria === 'string' ? { _id: criteria } : criteria;
|
||||
|
||||
return await this.model
|
||||
.deleteOne(typeof criteria === 'string' ? { _id: criteria } : criteria)
|
||||
.deleteOne({ ...filter, builtin: { $ne: true } })
|
||||
.exec();
|
||||
}
|
||||
|
||||
async deleteMany(criteria: TFilterQuery<T>): Promise<DeleteResult> {
|
||||
return await this.model.deleteMany(criteria);
|
||||
return await this.model.deleteMany({ ...criteria, builtin: { $ne: true } });
|
||||
}
|
||||
|
||||
async preCreateValidate(
|
||||
|
||||
10
api/src/utils/test/fixtures/nlpvalue.ts
vendored
10
api/src/utils/test/fixtures/nlpvalue.ts
vendored
@@ -18,35 +18,35 @@ export const nlpValueFixtures: NlpValueCreateDto[] = [
|
||||
entity: '0',
|
||||
value: 'positive',
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
builtin: false,
|
||||
doc: '',
|
||||
},
|
||||
{
|
||||
entity: '0',
|
||||
value: 'negative',
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
builtin: false,
|
||||
doc: '',
|
||||
},
|
||||
{
|
||||
entity: '1',
|
||||
value: 'jhon',
|
||||
expressions: ['john', 'joohn', 'jhonny'],
|
||||
builtin: true,
|
||||
builtin: false,
|
||||
doc: '',
|
||||
},
|
||||
{
|
||||
entity: '0',
|
||||
value: 'greeting',
|
||||
expressions: ['heello', 'Hello', 'hi', 'heyy'],
|
||||
builtin: true,
|
||||
builtin: false,
|
||||
doc: '',
|
||||
},
|
||||
{
|
||||
entity: '0',
|
||||
value: 'goodbye',
|
||||
expressions: ['bye', 'bye bye'],
|
||||
builtin: true,
|
||||
builtin: false,
|
||||
doc: '',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ModulesContainer } from '@nestjs/core';
|
||||
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { Socket } from 'socket.io';
|
||||
|
||||
import { SocketEventMetadataStorage } from '../storage/socket-event-metadata.storage';
|
||||
import { SocketRequest } from '../utils/socket-request';
|
||||
@@ -39,12 +41,21 @@ export class SocketEventDispatcherService implements OnModuleInit {
|
||||
private readonly modulesContainer: ModulesContainer,
|
||||
) {}
|
||||
|
||||
@OnEvent('hook:websocket:connection')
|
||||
handleConnection(client: Socket) {
|
||||
client.data.mutex = new Mutex();
|
||||
}
|
||||
|
||||
async handleEvent(
|
||||
socketMethod: SocketMethod,
|
||||
path: string,
|
||||
req: SocketRequest,
|
||||
res: SocketResponse,
|
||||
) {
|
||||
// Prevent racing conditions from the same socket
|
||||
const socketData = req.socket.data;
|
||||
const release = await socketData.mutex.acquire();
|
||||
|
||||
try {
|
||||
const handlers = this.routeHandlers[socketMethod];
|
||||
const foundHandler = Array.from(handlers.entries()).find(([key, _]) => {
|
||||
@@ -62,6 +73,8 @@ export class SocketEventDispatcherService implements OnModuleInit {
|
||||
return await handler(req, res);
|
||||
} catch (error) {
|
||||
return this.handleException(error, res);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@mui/x-data-grid": "^7.3.2",
|
||||
"@projectstorm/react-canvas-core": "^7.0.3",
|
||||
"@projectstorm/react-diagrams": "^7.0.4",
|
||||
"autolinker": "^4.1.5",
|
||||
"axios": "^1.7.7",
|
||||
"eazychart-css": "^0.2.1-alpha.0",
|
||||
"eazychart-react": "^0.8.0-alpha.0",
|
||||
@@ -38,17 +39,17 @@
|
||||
"normalizr": "^3.6.2",
|
||||
"notistack": "^3.0.1",
|
||||
"qs": "^6.12.1",
|
||||
"random-seed": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"react-i18next": "^14.1.1",
|
||||
"react-query": "^3.39.3",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"random-seed": "^0.3.0"
|
||||
"socket.io-client": "^4.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/random-seed": "^0.3.5",
|
||||
"@types/react": "18.3.2",
|
||||
"@types/react-dom": "^18",
|
||||
|
||||
@@ -8,13 +8,15 @@
|
||||
"fallback_block": "Fallback Block",
|
||||
"default_nlu_helper": "Default NLU Helper",
|
||||
"default_llm_helper": "Default LLM Helper",
|
||||
"default_storage_helper": "Default Storage Helper"
|
||||
"default_storage_helper": "Default Storage Helper",
|
||||
"default_nlu_penalty_factor": "NLU Penalty Factor"
|
||||
},
|
||||
"help": {
|
||||
"global_fallback": "Global fallback allows you to send custom messages when user entry does not match any of the block messages.",
|
||||
"fallback_message": "If no fallback block is selected, then one of these messages will be sent.",
|
||||
"default_nlu_helper": "The NLU helper is responsible for processing and understanding user inputs, including tasks like intent prediction, language detection, and entity recognition.",
|
||||
"default_llm_helper": "The LLM helper leverages advanced generative AI to perform tasks such as text generation, chat completion, and complex query responses.",
|
||||
"default_storage_helper": "The storage helper defines where to store attachment files. By default, the default local storage helper stores them locally, but you can choose to use Minio or any other storage solution."
|
||||
"default_storage_helper": "The storage helper defines where to store attachment files. By default, the default local storage helper stores them locally, but you can choose to use Minio or any other storage solution.",
|
||||
"default_nlu_penalty_factor": "The NLU penalty factor is a coefficient (between 0 and 1) applied exclusively to NLU-based entity matching. It reduces the score contribution of patterns that match broadly (e.g. using wildcard values like Any) rather than specific entity values. This helps the engine prioritize blocks triggered by more precise NLU matches, without affecting other matching strategies such as text, regex, or interaction triggers."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"message_is_required": "Message is required",
|
||||
"context_var_is_required": "You need to add a context variable",
|
||||
"invalid_list_limit": "List limit must be >=2 and <= 4",
|
||||
"invalid_carousel_limit": "List limit must be >=1 and <= 10",
|
||||
"no_content_type": "No content type available, please create one first",
|
||||
"invalid_max_fallback_attempt_limit": "Max fallback attempt limit must have positive value",
|
||||
"regex_is_invalid": "Regex is invalid",
|
||||
@@ -123,7 +124,7 @@
|
||||
"video_error": "Video not found",
|
||||
"missing_fields_error": "Please make sure that all required fields are filled",
|
||||
"weight_required_error": "Weight is required or invalid",
|
||||
"weight_positive_number_error": "Weight must be a strictly positive number"
|
||||
"weight_positive_number_error": "Weight must be a strictly positive number"
|
||||
},
|
||||
"menu": {
|
||||
"terms": "Terms of Use",
|
||||
@@ -490,7 +491,9 @@
|
||||
"original_text": "Original Text",
|
||||
"inputs": "Inputs",
|
||||
"outputs": "Outputs",
|
||||
"any": "- Any -"
|
||||
"any": "- Any -",
|
||||
"full_name": "First and last name",
|
||||
"password": "Password"
|
||||
},
|
||||
"placeholder": {
|
||||
"your_username": "Your username",
|
||||
@@ -498,7 +501,6 @@
|
||||
"your_password": "Your password",
|
||||
"username": "Username",
|
||||
"email": "E-mail address",
|
||||
"full_name": "First and last name",
|
||||
"password": "Password",
|
||||
"password2": "Confirm your password",
|
||||
"timezone": "Timezone",
|
||||
@@ -527,7 +529,9 @@
|
||||
"end_date": "End Date",
|
||||
"nlp_value": "Value",
|
||||
"type_message_here": "Type message here ....",
|
||||
"mark_as_default": "By Default"
|
||||
"mark_as_default": "By Default",
|
||||
"pattern": "Pattern",
|
||||
"full_name": "First and last name"
|
||||
},
|
||||
"button": {
|
||||
"login": "Sign In",
|
||||
|
||||
@@ -8,13 +8,15 @@
|
||||
"fallback_block": "Bloc de secours",
|
||||
"default_nlu_helper": "Utilitaire NLU par défaut",
|
||||
"default_llm_helper": "Utilitaire LLM par défaut",
|
||||
"default_storage_helper": "Utilitaire de stockage par défaut"
|
||||
"default_storage_helper": "Utilitaire de stockage par défaut",
|
||||
"default_nlu_penalty_factor": "Facteur de pénalité NLU"
|
||||
},
|
||||
"help": {
|
||||
"global_fallback": "La réponse de secours globale vous permet d'envoyer des messages personnalisés lorsque l'entrée de l'utilisateur ne correspond à aucun des messages des blocs.",
|
||||
"fallback_message": "Si aucun bloc de secours n'est sélectionné, l'un de ces messages sera envoyé.",
|
||||
"default_nlu_helper": "Utilitaire du traitement et de la compréhension des entrées des utilisateurs, incluant des tâches telles que la prédiction d'intention, la détection de langue et la reconnaissance d'entités.",
|
||||
"default_llm_helper": "Utilitaire responsable de l'intelligence artificielle générative avancée pour effectuer des tâches telles que la génération de texte, la complétion de chat et les réponses à des requêtes complexes.",
|
||||
"default_storage_helper": "Utilitaire de stockage définit l'emplacement où stocker les fichiers joints. Par défaut, le stockage local les conserve localement, mais vous pouvez choisir d'utiliser Minio ou toute autre solution de stockage."
|
||||
"default_storage_helper": "Utilitaire de stockage définit l'emplacement où stocker les fichiers joints. Par défaut, le stockage local les conserve localement, mais vous pouvez choisir d'utiliser Minio ou toute autre solution de stockage.",
|
||||
"default_nlu_penalty_factor": "Le facteur de pénalité NLU est un coefficient (entre 0 et 1) appliqué exclusivement aux correspondances d'entités basées sur NLU. Il réduit la contribution au score des motifs qui correspondent de manière générale (par exemple, en utilisant des valeurs génériques comme Any) plutôt que des valeurs d'entité spécifiques. Cela permet au chatbot de donner la priorité aux blocs déclenchés par des correspondances NLU plus précises, sans affecter d'autres stratégies de correspondance telles que le texte, les expressions regex ou les déclencheurs d'interaction."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"message_is_required": "Le message est requis",
|
||||
"context_var_is_required": "Vous devez ajouter une variable contextuelle",
|
||||
"invalid_list_limit": "La limite doit être >=2 et <= 4",
|
||||
"invalid_carousel_limit": "La limite doit être >=1 et <= 10",
|
||||
"no_content_type": "Il n'y a aucun type de contenu pour le moment, veuillez en ajouter un.",
|
||||
"invalid_max_fallback_attempt_limit": "La limite des tentatives de secours doit être un nombre positif.",
|
||||
"regex_is_invalid": "Le regex est invalide",
|
||||
@@ -491,7 +492,9 @@
|
||||
"original_text": "Texte par défaut",
|
||||
"inputs": "Ports d'entrée",
|
||||
"outputs": "Ports de sortie",
|
||||
"any": "- Toutes -"
|
||||
"any": "- Toutes -",
|
||||
"full_name": "Nom et Prénom",
|
||||
"password": "Mot de passe"
|
||||
},
|
||||
"placeholder": {
|
||||
"your_username": "Votre nom d'utilisateur",
|
||||
@@ -499,7 +502,6 @@
|
||||
"your_password": "Votre mot de passe",
|
||||
"username": "Nom d'utilisateur",
|
||||
"email": "Adresse e-mail",
|
||||
"full_name": "Nom et Prénom",
|
||||
"password": "Mot de passe",
|
||||
"password2": "Confirmez votre mot de passe",
|
||||
"timezone": "Fuseau horaire",
|
||||
@@ -528,7 +530,9 @@
|
||||
"end_date": "Date de fin",
|
||||
"nlp_value": "Valeur",
|
||||
"type_message_here": "Ecrivez quelque chose ici ....",
|
||||
"mark_as_default": "Par Défaut"
|
||||
"mark_as_default": "Par Défaut",
|
||||
"pattern": "Motif",
|
||||
"full_name": "Nom et Prénom"
|
||||
},
|
||||
"button": {
|
||||
"login": "Se connecter",
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
styled,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { ChangeEvent, DragEvent, FC, useState } from "react";
|
||||
import { ChangeEvent, DragEvent, FC, useId, useState } from "react";
|
||||
|
||||
import { useUpload } from "@/hooks/crud/useUpload";
|
||||
import { useDialogs } from "@/hooks/useDialogs";
|
||||
@@ -88,6 +88,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
const [attachment, setAttachment] = useState<IAttachment | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const uid = useId();
|
||||
const { t } = useTranslate();
|
||||
const dialogs = useDialogs();
|
||||
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
||||
@@ -154,11 +155,11 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
<HiddenInput
|
||||
onChange={handleChange}
|
||||
accept={accept}
|
||||
id="file-upload"
|
||||
id={`file-upload${uid}`}
|
||||
type="file"
|
||||
/>
|
||||
<FileUploadLabel
|
||||
htmlFor="file-upload"
|
||||
htmlFor={`file-upload${uid}`}
|
||||
isDragOver={isDragOver}
|
||||
onMouseEnter={() => setIsDragOver(true)}
|
||||
onMouseLeave={() => setIsDragOver(false)}
|
||||
@@ -221,7 +222,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
{
|
||||
defaultValues: { accept, onChange },
|
||||
},
|
||||
{ maxWidth: "xl" },
|
||||
{ maxWidth: "xl", isSingleton: true },
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -106,7 +106,7 @@ export const Login = () => {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label={t("placeholder.password")}
|
||||
label={t("label.password")}
|
||||
error={!!errors.password}
|
||||
required
|
||||
InputProps={{
|
||||
|
||||
@@ -198,7 +198,7 @@ export const Register = () => {
|
||||
</ContentItem>
|
||||
<ContentItem>
|
||||
<PasswordInput
|
||||
label={t("label.auth_pass")}
|
||||
label={t("label.password")}
|
||||
error={!!errors.password}
|
||||
required
|
||||
{...register("password")}
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ResetPassword = () => {
|
||||
</Typography>
|
||||
<PasswordInput
|
||||
autoFocus
|
||||
label={t("placeholder.password")}
|
||||
label={t("label.password")}
|
||||
error={!!errors.password}
|
||||
required
|
||||
InputProps={{
|
||||
|
||||
@@ -106,7 +106,7 @@ const AutoCompleteEntitySelect = <
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<Value, Label, Multiple>
|
||||
{...(options.length && { value })}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={label}
|
||||
multiple={multiple}
|
||||
|
||||
@@ -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.
|
||||
@@ -11,6 +11,7 @@ import Autocomplete, {
|
||||
AutocompleteProps,
|
||||
AutocompleteValue,
|
||||
} from "@mui/material/Autocomplete";
|
||||
import stringify from "fast-json-stable-stringify";
|
||||
import { forwardRef, useCallback, useMemo } from "react";
|
||||
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
@@ -112,7 +113,7 @@ const AutoCompleteSelect = <
|
||||
{...rest}
|
||||
ref={ref}
|
||||
size="small"
|
||||
key={JSON.stringify(value)}
|
||||
key={`${stringify(options)}_${stringify(value)}`}
|
||||
disabled={isDisabled}
|
||||
defaultValue={selected}
|
||||
multiple={multiple}
|
||||
|
||||
@@ -1,31 +1,100 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { TextFieldProps } from "@mui/material";
|
||||
import {
|
||||
debounce,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextFieldProps,
|
||||
} from "@mui/material";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
import { Adornment } from "./Adornment";
|
||||
import { Input } from "./Input";
|
||||
|
||||
export const FilterTextfield = (props: TextFieldProps) => {
|
||||
const { t } = useTranslate();
|
||||
export interface FilterTextFieldProps
|
||||
extends Omit<TextFieldProps, "value" | "onChange"> {
|
||||
onChange: (value: string) => void;
|
||||
delay?: number;
|
||||
clearable: boolean;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export const FilterTextfield = ({
|
||||
onChange: onSearch,
|
||||
defaultValue = "",
|
||||
delay = 500,
|
||||
clearable = true,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const isTyping = useRef(false);
|
||||
const toggleTyping = useMemo(
|
||||
() =>
|
||||
debounce((value: boolean) => {
|
||||
isTyping.current = value;
|
||||
}, delay * 2),
|
||||
[delay],
|
||||
);
|
||||
const debouncedSearch = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
onSearch?.(value);
|
||||
toggleTyping(false);
|
||||
}, delay),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[onSearch, delay],
|
||||
);
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
|
||||
toggleTyping(true);
|
||||
debouncedSearch(value);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[debouncedSearch],
|
||||
);
|
||||
const handleClear = useCallback(() => {
|
||||
debouncedSearch("");
|
||||
}, [debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
// Avoid infinite loop cycle (input => URL update => default value)
|
||||
if (defaultValue !== ref.current?.value && !isTyping.current) {
|
||||
ref.current && (ref.current.value = defaultValue);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValue]);
|
||||
|
||||
//TODO: replace the native delete text button by a styled custom button
|
||||
return (
|
||||
<Input
|
||||
type="search"
|
||||
inputRef={ref}
|
||||
InputProps={{
|
||||
startAdornment: <Adornment Icon={SearchIcon} />,
|
||||
endAdornment: clearable && (
|
||||
<InputAdornment position="end" onClick={handleClear}>
|
||||
<IconButton size="small" sx={{ marginRight: -1 }}>
|
||||
<ClearIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
placeholder={t("placeholder.keywords")}
|
||||
{...props}
|
||||
// value={inputValue}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,59 +1,40 @@
|
||||
/*
|
||||
* 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 React, { FC, SVGProps } from "react";
|
||||
import { FC, SVGProps } from "react";
|
||||
|
||||
const NoDataIcon: FC<SVGProps<SVGSVGElement>> = ({ ...props }) => {
|
||||
const NoDataIcon: FC<SVGProps<SVGSVGElement>> = ({ width = 96, ...props }) => {
|
||||
return (
|
||||
<svg
|
||||
height="150"
|
||||
viewBox="0 0 184 152"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
width={width}
|
||||
viewBox="0 0 452 257"
|
||||
aria-hidden
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
<title>No data</title>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<g transform="translate(24 31.67)">
|
||||
<ellipse
|
||||
fillOpacity=".8"
|
||||
fill="#F5F5F7"
|
||||
cx="67.797"
|
||||
cy="106.89"
|
||||
rx="67.797"
|
||||
ry="12.668"
|
||||
/>
|
||||
<path
|
||||
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
|
||||
fill="#AEB8C2"
|
||||
/>
|
||||
<path
|
||||
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
|
||||
fill="url(#linearGradient-1)"
|
||||
transform="translate(13.56)"
|
||||
/>
|
||||
<path
|
||||
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
|
||||
fill="#F5F5F7"
|
||||
/>
|
||||
<path
|
||||
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
|
||||
fill="#DCE0E6"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
|
||||
fill="#DCE0E6"
|
||||
/>
|
||||
<g transform="translate(149.65 15.383)" fill="#FFF">
|
||||
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815" />
|
||||
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
className="no-rows-primary"
|
||||
d="M348 69c-46.392 0-84 37.608-84 84s37.608 84 84 84 84-37.608 84-84-37.608-84-84-84Zm-104 84c0-57.438 46.562-104 104-104s104 46.562 104 104-46.562 104-104 104-104-46.562-104-104Z"
|
||||
/>
|
||||
<path
|
||||
className="no-rows-primary"
|
||||
d="M308.929 113.929c3.905-3.905 10.237-3.905 14.142 0l63.64 63.64c3.905 3.905 3.905 10.236 0 14.142-3.906 3.905-10.237 3.905-14.142 0l-63.64-63.64c-3.905-3.905-3.905-10.237 0-14.142Z"
|
||||
/>
|
||||
<path
|
||||
className="no-rows-primary"
|
||||
d="M308.929 191.711c-3.905-3.906-3.905-10.237 0-14.142l63.64-63.64c3.905-3.905 10.236-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142l-63.64 63.64c-3.905 3.905-10.237 3.905-14.142 0Z"
|
||||
/>
|
||||
<path
|
||||
className="no-rows-secondary"
|
||||
d="M0 10C0 4.477 4.477 0 10 0h380c5.523 0 10 4.477 10 10s-4.477 10-10 10H10C4.477 20 0 15.523 0 10ZM0 59c0-5.523 4.477-10 10-10h231c5.523 0 10 4.477 10 10s-4.477 10-10 10H10C4.477 69 0 64.523 0 59ZM0 106c0-5.523 4.477-10 10-10h203c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10ZM0 153c0-5.523 4.477-10 10-10h195.5c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10ZM0 200c0-5.523 4.477-10 10-10h203c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10ZM0 247c0-5.523 4.477-10 10-10h231c5.523 0 10 4.477 10 10s-4.477 10-10 10H10c-5.523 0-10-4.477-10-10Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
@@ -73,6 +73,12 @@ export const DataGrid = <T extends GridValidRowModel = any>({
|
||||
autoHeight={autoHeight}
|
||||
disableRowSelectionOnClick={disableRowSelectionOnClick}
|
||||
slots={slots}
|
||||
slotProps={{
|
||||
loadingOverlay: {
|
||||
variant: "linear-progress",
|
||||
noRowsVariant: "skeleton",
|
||||
},
|
||||
}}
|
||||
showCellVerticalBorder={showCellVerticalBorder}
|
||||
showColumnVerticalBorder={showColumnVerticalBorder}
|
||||
sx={sx}
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
/*
|
||||
* 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 { Grid, Typography } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
import NoDataIcon from "../svg/NoDataIcon";
|
||||
|
||||
const StyledGridOverlay = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
minHeight: "200px",
|
||||
"& .no-rows-primary": {
|
||||
fill: "#3D4751",
|
||||
...theme.applyStyles("light", {
|
||||
fill: "#AEB8C2",
|
||||
}),
|
||||
},
|
||||
"& .no-rows-secondary": {
|
||||
fill: "#1D2126",
|
||||
...theme.applyStyles("light", {
|
||||
fill: "#E8EAED",
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
export const NoDataOverlay = () => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
height: "fit-content",
|
||||
gap: 1,
|
||||
opacity: 0.5,
|
||||
paddingY: 1,
|
||||
}}
|
||||
>
|
||||
<StyledGridOverlay>
|
||||
<NoDataIcon />
|
||||
<Grid item>
|
||||
<Typography
|
||||
style={{
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
{t("label.no_data")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box sx={{ mt: 2 }}>{t("label.no_data")}</Box>
|
||||
</StyledGridOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { Avatar, Box } from "@mui/material";
|
||||
import UiChatWidget from "hexabot-chat-widget/src/UiChatWidget";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
@@ -17,7 +17,6 @@ import { useConfig } from "@/hooks/useConfig";
|
||||
import { useSetting } from "@/hooks/useSetting";
|
||||
import i18n from "@/i18n/config";
|
||||
import { EntityType, RouterType } from "@/services/types";
|
||||
import { generateId } from "@/utils/generateId";
|
||||
|
||||
import { ChatWidgetHeader } from "./ChatWidgetHeader";
|
||||
|
||||
@@ -30,11 +29,10 @@ export const ChatWidget = () => {
|
||||
const isVisualEditor = pathname.startsWith(`/${RouterType.VISUAL_EDITOR}`);
|
||||
const allowedDomainsSetting = useSetting(SETTING_TYPE, "allowed_domains");
|
||||
const themeColorSetting = useSetting(SETTING_TYPE, "theme_color");
|
||||
const [key, setKey] = useState(generateId());
|
||||
|
||||
useEffect(() => {
|
||||
setKey(generateId());
|
||||
}, [allowedDomainsSetting, themeColorSetting]);
|
||||
const key = useMemo(
|
||||
() => `${allowedDomainsSetting}_${themeColorSetting}`,
|
||||
[allowedDomainsSetting, themeColorSetting],
|
||||
);
|
||||
|
||||
return isAuthenticated ? (
|
||||
<Box
|
||||
|
||||
@@ -43,9 +43,12 @@ export const Categories = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ICategory>({
|
||||
$iLike: ["label"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ICategory>(
|
||||
{
|
||||
$iLike: ["label"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CATEGORY },
|
||||
{
|
||||
@@ -142,7 +145,7 @@ export const Categories = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CATEGORY, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -40,9 +40,12 @@ export const ContentTypes = () => {
|
||||
const router = useRouter();
|
||||
const dialogs = useDialogs();
|
||||
// data fetching
|
||||
const { onSearch, searchPayload } = useSearch<IContentType>({
|
||||
$iLike: ["name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IContentType>(
|
||||
{
|
||||
$iLike: ["name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CONTENT_TYPE },
|
||||
{
|
||||
@@ -99,7 +102,7 @@ export const ContentTypes = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CONTENT_TYPE, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -57,10 +57,13 @@ export const Contents = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dialogs = useDialogs();
|
||||
// data fetching
|
||||
const { onSearch, searchPayload } = useSearch<IContent>({
|
||||
$eq: [{ entity: String(query.id) }],
|
||||
$iLike: ["title"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IContent>(
|
||||
{
|
||||
$eq: [{ entity: String(query.id) }],
|
||||
$iLike: ["title"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const hasPermission = useHasPermission();
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CONTENT, format: Format.FULL },
|
||||
@@ -157,7 +160,7 @@ export const Contents = () => {
|
||||
>
|
||||
<Grid justifyContent="flex-end" gap={1} container alignItems="center">
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? (
|
||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||
|
||||
@@ -43,9 +43,12 @@ export const ContextVars = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<IContextVar>({
|
||||
$iLike: ["label"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IContextVar>(
|
||||
{
|
||||
$iLike: ["label"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.CONTEXT_VAR },
|
||||
{
|
||||
@@ -176,7 +179,7 @@ export const ContextVars = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.CONTEXT_VAR, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { Title } from "@/layout/content/Title";
|
||||
import { EntityType, RouterType } from "@/services/types";
|
||||
import { normalizeDate } from "@/utils/date";
|
||||
import { extractQueryParamsUrl } from "@/utils/URL";
|
||||
|
||||
import { getAvatarSrc } from "../helpers/mapMessages";
|
||||
import { useChat } from "../hooks/ChatContext";
|
||||
@@ -33,8 +32,8 @@ export const SubscribersList = (props: {
|
||||
searchPayload: any;
|
||||
assignedTo: AssignedTo;
|
||||
}) => {
|
||||
const { query, push } = useRouter();
|
||||
const subscriber = query.subscriber?.toString() || null;
|
||||
const router = useRouter();
|
||||
const subscriber = router.query.subscriber?.toString() || null;
|
||||
const { apiUrl } = useConfig();
|
||||
const { t, i18n } = useTranslate();
|
||||
const chat = useChat();
|
||||
@@ -75,10 +74,17 @@ export const SubscribersList = (props: {
|
||||
<Conversation
|
||||
onClick={() => {
|
||||
chat.setSubscriberId(subscriber.id);
|
||||
push({
|
||||
pathname: `/${RouterType.INBOX}/subscribers/${subscriber.id}`,
|
||||
query: extractQueryParamsUrl(window.location.href),
|
||||
});
|
||||
router.push(
|
||||
{
|
||||
pathname: `/${RouterType.INBOX}/subscribers/[subscriber]`,
|
||||
query: {
|
||||
...router.query,
|
||||
subscriber: subscriber.id,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}}
|
||||
className="changeColor"
|
||||
key={subscriber.id}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Message, MessageModel } from "@chatscope/chat-ui-kit-react";
|
||||
import MenuRoundedIcon from "@mui/icons-material/MenuRounded";
|
||||
import ReplyIcon from "@mui/icons-material/Reply";
|
||||
import { Chip, Grid } from "@mui/material";
|
||||
import Autolinker from "autolinker";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { ROUTES } from "@/services/api.class";
|
||||
@@ -60,6 +61,31 @@ export function isSubsequent(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Detects URLs in text and converts them to clickable links using Autolinker
|
||||
*/
|
||||
function formatMessageText(text: string): ReactNode {
|
||||
try {
|
||||
const linkedText = Autolinker.link(text, {
|
||||
className: "chat-link",
|
||||
newWindow: true,
|
||||
truncate: { length: 50, location: "middle" },
|
||||
stripPrefix: false,
|
||||
sanitizeHtml: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: linkedText,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} catch (error) {
|
||||
return <div>{text}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description this function constructs the message children basen on message type
|
||||
*/
|
||||
@@ -79,7 +105,9 @@ export function getMessageContent(
|
||||
|
||||
if ("text" in message) {
|
||||
content.push(
|
||||
<Message.TextContent key={messageEntity.id} text={message.text} />,
|
||||
<Message.CustomContent key={messageEntity.id}>
|
||||
{formatMessageText(message.text)}
|
||||
</Message.CustomContent>,
|
||||
);
|
||||
}
|
||||
let chips: { title: string }[] = [];
|
||||
|
||||
@@ -105,3 +105,14 @@ div .cs-message-input__content-editor-container,
|
||||
padding: 15px 5px !important;
|
||||
box-shadow: 0px 4px 10px 10px rgba(0, 0, 0, 0.066);
|
||||
}
|
||||
|
||||
/* Styles for autolinked chat messages */
|
||||
.chat-link {
|
||||
color: inherit !important;
|
||||
text-decoration: underline !important;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
.chat-link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { MainContainer, Search, Sidebar } from "@chatscope/chat-ui-kit-react";
|
||||
import { MainContainer, Sidebar } from "@chatscope/chat-ui-kit-react";
|
||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
import { Grid, MenuItem } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
@@ -26,9 +27,12 @@ import { AssignedTo } from "./types";
|
||||
|
||||
export const Inbox = () => {
|
||||
const { t } = useTranslate();
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>({
|
||||
$or: ["first_name", "last_name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>(
|
||||
{
|
||||
$or: ["first_name", "last_name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const [channels, setChannels] = useState<string[]>([]);
|
||||
const [assignment, setAssignment] = useState<AssignedTo>(AssignedTo.ALL);
|
||||
|
||||
@@ -46,13 +50,10 @@ export const Inbox = () => {
|
||||
<Grid item width="100%" height="100%" overflow="hidden">
|
||||
<MainContainer style={{ height: "100%" }}>
|
||||
<Sidebar position="left">
|
||||
<Grid paddingX={1} paddingTop={1}>
|
||||
<Search
|
||||
value={searchText}
|
||||
onClearClick={() => onSearch("")}
|
||||
className="changeColor"
|
||||
onChange={(v) => onSearch(v)}
|
||||
placeholder="Search..."
|
||||
<Grid paddingX={1} paddingTop={2} paddingBottom={1} mx={1}>
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
|
||||
@@ -42,9 +42,12 @@ export const Labels = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ILabel>({
|
||||
$or: ["name", "title"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ILabel>(
|
||||
{
|
||||
$or: ["name", "title"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.LABEL, format: Format.FULL },
|
||||
{
|
||||
@@ -173,7 +176,7 @@ export const Labels = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.LABEL, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -43,9 +43,12 @@ export const Languages = () => {
|
||||
const dialogs = useDialogs();
|
||||
const queryClient = useQueryClient();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ILanguage>({
|
||||
$or: ["title", "code"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ILanguage>(
|
||||
{
|
||||
$or: ["title", "code"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps, refetch } = useFind(
|
||||
{ entity: EntityType.LANGUAGE },
|
||||
{
|
||||
@@ -197,7 +200,7 @@ export const Languages = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -37,9 +37,13 @@ type MediaLibraryProps = {
|
||||
export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||
const { t } = useTranslate();
|
||||
const formatFileSize = useFormattedFileSize();
|
||||
const { onSearch, searchPayload } = useSearch<IAttachment>({
|
||||
$iLike: ["name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IAttachment>(
|
||||
{
|
||||
$iLike: ["name"],
|
||||
},
|
||||
// Sync URL only in the media library page (not the modal)
|
||||
{ syncUrl: !onSelect },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.ATTACHMENT },
|
||||
{
|
||||
@@ -151,7 +155,7 @@ export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</PageHeader>
|
||||
|
||||
@@ -80,9 +80,12 @@ const NlpEntity = () => {
|
||||
},
|
||||
});
|
||||
const [selectedNlpEntities, setSelectedNlpEntities] = useState<string[]>([]);
|
||||
const { onSearch, searchPayload } = useSearch<INlpEntity>({
|
||||
$or: ["name", "doc"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<INlpEntity>(
|
||||
{
|
||||
$or: ["name", "doc"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps: nlpEntityGrid } = useFind(
|
||||
{
|
||||
entity: EntityType.NLP_ENTITY,
|
||||
@@ -224,7 +227,7 @@ const NlpEntity = () => {
|
||||
flexShrink={0}
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
|
||||
{hasPermission(EntityType.NLP_ENTITY, PermissionAction.CREATE) ? (
|
||||
@@ -267,6 +270,7 @@ const NlpEntity = () => {
|
||||
<DataGrid
|
||||
columns={nlpEntityColumns}
|
||||
{...nlpEntityGrid}
|
||||
isRowSelectable={({ row }) => !row.builtin}
|
||||
checkboxSelection
|
||||
onRowSelectionModelChange={handleSelectionChange}
|
||||
/>
|
||||
|
||||
@@ -86,13 +86,16 @@ export default function NlpSample() {
|
||||
EntityType.NLP_SAMPLE_ENTITY,
|
||||
);
|
||||
const getLanguageFromCache = useGetFromCache(EntityType.LANGUAGE);
|
||||
const { onSearch, searchPayload } = useSearch<INlpSample>({
|
||||
$eq: [
|
||||
...(type !== "all" ? [{ type }] : []),
|
||||
...(language ? [{ language }] : []),
|
||||
],
|
||||
$iLike: ["text"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<INlpSample>(
|
||||
{
|
||||
$eq: [
|
||||
...(type !== "all" ? [{ type }] : []),
|
||||
...(language ? [{ language }] : []),
|
||||
],
|
||||
$iLike: ["text"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { mutate: deleteNlpSample } = useDelete(EntityType.NLP_SAMPLE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
@@ -317,6 +320,7 @@ export default function NlpSample() {
|
||||
>
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
defaultValue={searchText}
|
||||
fullWidth={false}
|
||||
sx={{ minWidth: "256px" }}
|
||||
/>
|
||||
|
||||
@@ -51,10 +51,13 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
entity: EntityType.NLP_ENTITY,
|
||||
format: Format.FULL,
|
||||
});
|
||||
const { onSearch, searchPayload } = useSearch<INlpValue>({
|
||||
$eq: [{ entity: entityId }],
|
||||
$or: ["doc", "value"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<INlpValue>(
|
||||
{
|
||||
$eq: [{ entity: entityId }],
|
||||
$or: ["doc", "value"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.NLP_VALUE, format: Format.FULL },
|
||||
{
|
||||
@@ -228,7 +231,10 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
sx={{ width: "max-content", gap: 1 }}
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Grid>
|
||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||
{hasPermission(
|
||||
|
||||
@@ -168,7 +168,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
||||
</ContentItem>
|
||||
<ContentItem>
|
||||
<PasswordInput
|
||||
label={t("placeholder.password")}
|
||||
label={t("label.password")}
|
||||
{...register("password")}
|
||||
required
|
||||
error={!!errors.password}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -40,9 +40,12 @@ export const Roles = () => {
|
||||
const { toast } = useToast();
|
||||
const dialogs = useDialogs();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<IRole>({
|
||||
$iLike: ["name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IRole>(
|
||||
{
|
||||
$iLike: ["name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.ROLE },
|
||||
{
|
||||
@@ -140,7 +143,7 @@ export const Roles = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.ROLE, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
|
||||
@@ -118,8 +118,8 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
|
||||
entity={EntityType.BLOCK}
|
||||
format={Format.BASIC}
|
||||
labelKey="name"
|
||||
label={t("label.fallback_block")}
|
||||
helperText={t("help.fallback_block")}
|
||||
label={t("label.fallback_message")}
|
||||
helperText={t("help.fallback_message")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected, ..._) => onChange(selected?.id || "")}
|
||||
{...rest}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -43,10 +43,13 @@ export const Subscribers = () => {
|
||||
{ hasCount: false },
|
||||
);
|
||||
const [labelFilter, setLabelFilter] = useState<string>("");
|
||||
const { onSearch, searchPayload } = useSearch<ISubscriber>({
|
||||
$eq: labelFilter ? [{ labels: [labelFilter] }] : [],
|
||||
$or: ["first_name", "last_name"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ISubscriber>(
|
||||
{
|
||||
$eq: labelFilter ? [{ labels: [labelFilter] }] : [],
|
||||
$or: ["first_name", "last_name"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps } = useFind(
|
||||
{ entity: EntityType.SUBSCRIBER, format: Format.FULL },
|
||||
{ params: searchPayload },
|
||||
@@ -172,7 +175,11 @@ export const Subscribers = () => {
|
||||
flexWrap="nowrap"
|
||||
width="50%"
|
||||
>
|
||||
<FilterTextfield onChange={onSearch} fullWidth={true} />
|
||||
<FilterTextfield
|
||||
onChange={onSearch}
|
||||
fullWidth={true}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
<Input
|
||||
select
|
||||
label={t("label.labels")}
|
||||
|
||||
@@ -44,9 +44,12 @@ export const Translations = () => {
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const { onSearch, searchPayload } = useSearch<ITranslation>({
|
||||
$iLike: ["str"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<ITranslation>(
|
||||
{
|
||||
$iLike: ["str"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { dataGridProps, refetch: refreshTranslations } = useFind(
|
||||
{ entity: EntityType.TRANSLATION },
|
||||
{
|
||||
@@ -152,7 +155,7 @@ export const Translations = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
|
||||
@@ -76,7 +76,7 @@ export const EditUserForm: FC<ComponentFormProps<IUser, IRole[]>> = ({
|
||||
<ContentItem>
|
||||
<Input
|
||||
disabled
|
||||
label={t("label.auth_user")}
|
||||
label={t("label.full_name")}
|
||||
value={user ? getFullName(user) : undefined}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
|
||||
@@ -53,9 +53,12 @@ export const Users = () => {
|
||||
},
|
||||
});
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<IUser>({
|
||||
$or: ["first_name", "last_name", "email"],
|
||||
});
|
||||
const { onSearch, searchPayload, searchText } = useSearch<IUser>(
|
||||
{
|
||||
$or: ["first_name", "last_name", "email"],
|
||||
},
|
||||
{ syncUrl: true },
|
||||
);
|
||||
const { data: roles } = useFind(
|
||||
{
|
||||
entity: EntityType.ROLE,
|
||||
@@ -102,7 +105,7 @@ export const Users = () => {
|
||||
headerName: t("label.name"),
|
||||
sortable: false,
|
||||
disableColumnMenu: true,
|
||||
valueGetter: (params, val) => `${val.first_name} ${val.last_name}`,
|
||||
valueGetter: (_params, val) => `${val.first_name} ${val.last_name}`,
|
||||
headerAlign: "left",
|
||||
renderHeader,
|
||||
},
|
||||
@@ -198,7 +201,7 @@ export const Users = () => {
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
<FilterTextfield onChange={onSearch} defaultValue={searchText} />
|
||||
</Grid>
|
||||
{!ssoEnabled &&
|
||||
hasPermission(EntityType.USER, PermissionAction.CREATE) ? (
|
||||
|
||||
@@ -47,7 +47,7 @@ const ListMessageForm = () => {
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const contentTypeId = watch("options.content.entity");
|
||||
const displayMode = watch("options.content.display_mode");
|
||||
const displayMode = watch("options.content.display");
|
||||
const { data: contentType } = useGet(contentTypeId, {
|
||||
entity: EntityType.CONTENT_TYPE,
|
||||
});
|
||||
@@ -61,10 +61,9 @@ const ListMessageForm = () => {
|
||||
<FormControl>
|
||||
<FormLabel>{t("label.display_mode")}</FormLabel>
|
||||
<Controller
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
defaultValue={content?.display || "list"}
|
||||
name="options.content.display_mode"
|
||||
defaultValue={content?.display || OutgoingMessageFormat.list}
|
||||
name="options.content.display"
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
{[
|
||||
@@ -75,10 +74,7 @@ const ListMessageForm = () => {
|
||||
key={display}
|
||||
value={display}
|
||||
control={
|
||||
<Radio
|
||||
defaultChecked={display === content?.display}
|
||||
{...register("options.content.display")}
|
||||
/>
|
||||
<Radio defaultChecked={display === content?.display} />
|
||||
}
|
||||
label={t(`label.${display}`)}
|
||||
/>
|
||||
@@ -117,16 +113,26 @@ const ListMessageForm = () => {
|
||||
label={t("label.content_limit")}
|
||||
type="number"
|
||||
inputProps={{
|
||||
maxLength: 25,
|
||||
step: "1",
|
||||
min: 2,
|
||||
max: 4,
|
||||
maxLength: 2,
|
||||
step: 1,
|
||||
min: displayMode === OutgoingMessageFormat.list ? 2 : 1,
|
||||
max: displayMode === OutgoingMessageFormat.list ? 4 : 10,
|
||||
}}
|
||||
{...register("options.content.limit", {
|
||||
validate: {
|
||||
min: (value) =>
|
||||
(value && value >= 2 && value <= 4) ||
|
||||
t("message.invalid_list_limit"),
|
||||
limitRange: (value) => {
|
||||
if (
|
||||
displayMode === OutgoingMessageFormat.list &&
|
||||
(value < 2 || value > 4)
|
||||
) {
|
||||
return t("message.invalid_list_limit");
|
||||
} else if (
|
||||
displayMode === OutgoingMessageFormat.carousel &&
|
||||
(value < 1 || value > 10)
|
||||
) {
|
||||
return t("message.invalid_carousel_limit");
|
||||
}
|
||||
},
|
||||
},
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
@@ -182,7 +188,7 @@ const ListMessageForm = () => {
|
||||
}}
|
||||
defaultValue={content?.fields?.title}
|
||||
render={({ field }) => {
|
||||
const { onChange, value, ...rest } = field;
|
||||
const { onChange, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(
|
||||
({ type }) => ContentFieldType.TEXT === type,
|
||||
);
|
||||
@@ -194,7 +200,6 @@ const ListMessageForm = () => {
|
||||
labelKey="label"
|
||||
label={t("label.title")}
|
||||
multiple={false}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
error={!!errors?.options?.["content"]?.fields?.title}
|
||||
@@ -212,7 +217,7 @@ const ListMessageForm = () => {
|
||||
control={control}
|
||||
defaultValue={content?.fields?.subtitle}
|
||||
render={({ field }) => {
|
||||
const { onChange, value, ...rest } = field;
|
||||
const { onChange, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(
|
||||
({ type }) =>
|
||||
ContentFieldType.TEXT === type ||
|
||||
@@ -227,7 +232,6 @@ const ListMessageForm = () => {
|
||||
label={t("label.subtitle")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
@@ -240,7 +244,7 @@ const ListMessageForm = () => {
|
||||
control={control}
|
||||
defaultValue={content?.fields?.image_url}
|
||||
render={({ field }) => {
|
||||
const { onChange, value, ...rest } = field;
|
||||
const { onChange, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(({ type }) =>
|
||||
[ContentFieldType.FILE].includes(type),
|
||||
);
|
||||
@@ -253,7 +257,6 @@ const ListMessageForm = () => {
|
||||
label={t("label.image_url")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
@@ -266,7 +269,7 @@ const ListMessageForm = () => {
|
||||
control={control}
|
||||
defaultValue={content?.fields?.url}
|
||||
render={({ field }) => {
|
||||
const { onChange, value, ...rest } = field;
|
||||
const { onChange, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(({ type }) =>
|
||||
[ContentFieldType.URL].includes(type),
|
||||
);
|
||||
@@ -279,7 +282,6 @@ const ListMessageForm = () => {
|
||||
label={t("label.url")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
@@ -86,6 +86,7 @@ export const useFind = <
|
||||
);
|
||||
}
|
||||
},
|
||||
keepPreviousData: true,
|
||||
...otherOptions,
|
||||
});
|
||||
const data = (ids || [])
|
||||
@@ -99,7 +100,7 @@ export const useFind = <
|
||||
dataGridProps: {
|
||||
...dataGridPaginationProps,
|
||||
rows: data || [],
|
||||
loading: normalizedQuery.isLoading,
|
||||
loading: normalizedQuery.isLoading || normalizedQuery.isFetching,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,6 +74,7 @@ export const useInfiniteFind = <
|
||||
|
||||
return result;
|
||||
},
|
||||
keepPreviousData: true,
|
||||
...(otherOptions || {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,9 +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 { debounce } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from "react";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
import {
|
||||
TBuildInitialParamProps,
|
||||
@@ -16,6 +14,8 @@ import {
|
||||
TParamItem,
|
||||
} from "@/types/search.types";
|
||||
|
||||
import { useUrlQueryParam } from "./useUrlQueryParam";
|
||||
|
||||
const buildOrParams = <T,>({ params, searchText }: TBuildParamProps<T>) => ({
|
||||
or: params?.map((field) => ({
|
||||
[field]: { contains: searchText },
|
||||
@@ -52,49 +52,39 @@ const buildNeqInitialParams = <T,>({
|
||||
{},
|
||||
);
|
||||
|
||||
export const useSearch = <T,>(params: TParamItem<T>) => {
|
||||
const router = useRouter();
|
||||
const [searchText, setSearchText] = useState<string>(
|
||||
(router.query.search as string) || "",
|
||||
);
|
||||
interface SearchHookOptions {
|
||||
syncUrl?: boolean;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.search !== searchText) {
|
||||
setSearchText((router.query.search as string) || "");
|
||||
}
|
||||
}, [router.query.search]);
|
||||
|
||||
const updateQueryParams = useCallback(
|
||||
debounce(async (newSearchText: string) => {
|
||||
await router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, search: newSearchText || undefined },
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}, 300),
|
||||
[router],
|
||||
);
|
||||
const onSearch = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string,
|
||||
) => {
|
||||
const newSearchText = typeof e === "string" ? e : e.target.value;
|
||||
|
||||
setSearchText(newSearchText);
|
||||
updateQueryParams(newSearchText);
|
||||
};
|
||||
export const useSearch = <T,>(
|
||||
params: TParamItem<T>,
|
||||
options: SearchHookOptions = { syncUrl: false },
|
||||
) => {
|
||||
const { syncUrl } = options;
|
||||
const [searchQuery, setSearchQuery] = useUrlQueryParam("search", "");
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const {
|
||||
$eq: eqInitialParams,
|
||||
$iLike: iLikeParams,
|
||||
$neq: neqInitialParams,
|
||||
$or: orParams,
|
||||
} = params;
|
||||
const searchText = syncUrl ? searchQuery : search;
|
||||
|
||||
return {
|
||||
searchText,
|
||||
onSearch,
|
||||
onSearch: (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string,
|
||||
) => {
|
||||
const newValue =
|
||||
typeof e === "object" ? e.target.value.toString() : e.toString();
|
||||
|
||||
if (syncUrl) {
|
||||
setSearchQuery(newValue);
|
||||
} else {
|
||||
setSearch(newValue);
|
||||
}
|
||||
},
|
||||
searchPayload: {
|
||||
where: {
|
||||
...buildEqInitialParams({ initialParams: eqInitialParams }),
|
||||
|
||||
119
frontend/src/hooks/useUrlQueryParam.ts
Normal file
119
frontend/src/hooks/useUrlQueryParam.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
type QueryParamSerializer<T> = {
|
||||
parse: (raw: string | string[] | undefined) => T;
|
||||
stringify: (val: T) => string | string[] | undefined;
|
||||
};
|
||||
|
||||
export function defaultSerializer<T = string>(): QueryParamSerializer<T> {
|
||||
return {
|
||||
parse: (raw) => {
|
||||
if (Array.isArray(raw)) return raw[0] as unknown as T;
|
||||
if (typeof raw === "undefined") return "" as unknown as T;
|
||||
|
||||
return raw as unknown as T;
|
||||
},
|
||||
stringify: (val) => val as unknown as string,
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanSerializer(): QueryParamSerializer<boolean> {
|
||||
return {
|
||||
parse: (raw) => raw === "true",
|
||||
stringify: (val) => (val ? "true" : "false"),
|
||||
};
|
||||
}
|
||||
|
||||
export function numberSerializer(): QueryParamSerializer<number> {
|
||||
return {
|
||||
parse: (raw) => {
|
||||
const value = Number(Array.isArray(raw) ? raw[0] : raw);
|
||||
|
||||
return isNaN(value) ? 0 : value;
|
||||
},
|
||||
stringify: (val) => val.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function arraySerializer(): QueryParamSerializer<string[]> {
|
||||
return {
|
||||
parse: (raw) => (Array.isArray(raw) ? raw : raw ? [raw] : []),
|
||||
stringify: (val) => val,
|
||||
};
|
||||
}
|
||||
|
||||
export const useUrlQueryParam = <T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
serializer: QueryParamSerializer<T> = defaultSerializer(),
|
||||
): [T, (val: T) => void] => {
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
// On initial load, use query or default
|
||||
const initial = router.query[key];
|
||||
|
||||
if (initial === undefined) return defaultValue;
|
||||
// parse value if needed (e.g., numbers)
|
||||
try {
|
||||
return serializer.parse(initial) as T;
|
||||
} catch {
|
||||
return initial as unknown as T;
|
||||
}
|
||||
});
|
||||
|
||||
// Sync from URL to state on changes (including initial load when ready)
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return;
|
||||
const urlValue = router.query[key];
|
||||
let parsedVal: T = defaultValue;
|
||||
|
||||
if (urlValue !== undefined) {
|
||||
try {
|
||||
parsedVal = serializer.parse(urlValue);
|
||||
} catch {
|
||||
parsedVal = urlValue as unknown as T;
|
||||
}
|
||||
} else {
|
||||
parsedVal = defaultValue;
|
||||
}
|
||||
if (parsedVal !== value) {
|
||||
setValue(parsedVal);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady, router.query[key]]);
|
||||
|
||||
// Update URL when state changes
|
||||
const updateValue = useCallback(
|
||||
(val: T) => {
|
||||
setValue(val);
|
||||
if (!router.isReady) return;
|
||||
const newQuery = { ...router.query };
|
||||
|
||||
if (
|
||||
val === defaultValue ||
|
||||
val === undefined ||
|
||||
serializer.stringify(val) === ""
|
||||
) {
|
||||
delete newQuery[key];
|
||||
} else {
|
||||
newQuery[key] = serializer.stringify(val);
|
||||
}
|
||||
router.push({ pathname: router.pathname, query: newQuery }, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[router, key, defaultValue],
|
||||
);
|
||||
|
||||
return [value, updateValue];
|
||||
};
|
||||
@@ -6,8 +6,6 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import qs from "qs";
|
||||
|
||||
export const buildURL = (baseUrl: string, relativePath: string): string => {
|
||||
try {
|
||||
return new URL(relativePath).toString();
|
||||
@@ -39,12 +37,3 @@ export const isAbsoluteUrl = (value: string = ""): boolean => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// todo: in the future we might need to extract this logic into a hook
|
||||
export const extractQueryParamsUrl = (fullUrl: string): string => {
|
||||
const extractedQueryParams = qs.parse(new URL(fullUrl).search, {
|
||||
ignoreQueryPrefix: true,
|
||||
});
|
||||
|
||||
return qs.stringify(extractedQueryParams);
|
||||
};
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hexabot",
|
||||
"version": "2.2.7",
|
||||
"version": "2.2.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hexabot",
|
||||
"version": "2.2.7",
|
||||
"version": "2.2.8",
|
||||
"license": "AGPL-3.0-only",
|
||||
"workspaces": [
|
||||
"frontend",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
a.chatLink {
|
||||
color: inherit !important;
|
||||
.sc-message--text-content a.chatLink {
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--launcher-color);
|
||||
}
|
||||
}
|
||||
p.sc-message--text-content {
|
||||
margin: 0 !important;
|
||||
|
||||
@@ -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.
|
||||
@@ -34,6 +34,7 @@ const TextMessage: React.FC<TextMessageProps> = ({ message }) => {
|
||||
messageTextRef.current.innerHTML = Autolinker.link(text, {
|
||||
className: "chatLink",
|
||||
truncate: { length: 50, location: "smart" },
|
||||
sanitizeHtml: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -50,6 +51,7 @@ const TextMessage: React.FC<TextMessageProps> = ({ message }) => {
|
||||
style={{
|
||||
color: colors.text,
|
||||
backgroundColor: colors.bg,
|
||||
["--launcher-color" as string]: allColors.launcher.bg,
|
||||
}}
|
||||
>
|
||||
<p className="sc-message--text-content" ref={messageTextRef}>
|
||||
|
||||
Reference in New Issue
Block a user