mirror of
https://github.com/hexastack/hexabot
synced 2025-04-08 15:04:54 +00:00
feat: wrap up translation logic
This commit is contained in:
parent
16e7431d83
commit
ecb8d9745a
@ -44,7 +44,7 @@ import idPlugin from './utils/schema-plugin/id.plugin';
|
||||
import { WebsocketModule } from './websocket/websocket.module';
|
||||
|
||||
const i18nOptions: I18nOptions = {
|
||||
fallbackLanguage: config.chatbot.lang.default,
|
||||
fallbackLanguage: 'en',
|
||||
loaderOptions: {
|
||||
path: path.join(__dirname, '/config/i18n/'),
|
||||
watch: true,
|
||||
|
@ -19,7 +19,10 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
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';
|
||||
@ -86,6 +89,7 @@ describe('BlockController', () => {
|
||||
UserModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@ -97,6 +101,7 @@ describe('BlockController', () => {
|
||||
UserRepository,
|
||||
RoleRepository,
|
||||
PermissionRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
LabelService,
|
||||
CategoryService,
|
||||
@ -105,6 +110,7 @@ describe('BlockController', () => {
|
||||
UserService,
|
||||
RoleService,
|
||||
PermissionService,
|
||||
LanguageService,
|
||||
PluginService,
|
||||
LoggerService,
|
||||
{
|
||||
|
@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
@ -28,7 +29,10 @@ import OfflineHandler from '@/extensions/channels/offline/index.channel';
|
||||
import { OFFLINE_CHANNEL_NAME } from '@/extensions/channels/offline/settings';
|
||||
import { Offline } from '@/extensions/channels/offline/types';
|
||||
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
@ -92,6 +96,7 @@ describe('BlockService', () => {
|
||||
ContentModel,
|
||||
AttachmentModel,
|
||||
LabelModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@ -100,11 +105,13 @@ describe('BlockService', () => {
|
||||
ContentTypeRepository,
|
||||
ContentRepository,
|
||||
AttachmentRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
CategoryService,
|
||||
ContentTypeService,
|
||||
ContentService,
|
||||
AttachmentService,
|
||||
LanguageService,
|
||||
{
|
||||
provide: PluginService,
|
||||
useValue: {},
|
||||
@ -130,6 +137,14 @@ describe('BlockService', () => {
|
||||
},
|
||||
},
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
blockService = module.get<BlockService>(BlockService);
|
||||
|
@ -14,6 +14,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { Nlp } from '@/nlp/lib/types';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
@ -44,6 +45,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
private readonly pluginService: PluginService,
|
||||
private readonly logger: LoggerService,
|
||||
protected readonly i18n: I18nService,
|
||||
protected readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
@ -108,12 +110,9 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
// Check & catch user language through NLP
|
||||
const nlp = event.getNLP();
|
||||
if (nlp) {
|
||||
const settings = await this.settingService.getSettings();
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const lang = nlp.entities.find((e) => e.entity === 'language');
|
||||
if (
|
||||
lang &&
|
||||
settings.nlp_settings.languages.indexOf(lang.value) !== -1
|
||||
) {
|
||||
if (lang && Object.keys(languages).indexOf(lang.value) !== -1) {
|
||||
const profile = event.getSender();
|
||||
profile.language = lang.value;
|
||||
event.setSender(profile);
|
||||
@ -369,12 +368,11 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
* @returns The text message translated and tokens being replaces with values
|
||||
*/
|
||||
processText(text: string, context: Context, settings: Settings): string {
|
||||
const lang =
|
||||
context && context.user && context.user.language
|
||||
? context.user.language
|
||||
: settings.nlp_settings.default_lang;
|
||||
// Translate
|
||||
text = this.i18n.t(text, { lang, defaultValue: text });
|
||||
text = this.i18n.t(text, {
|
||||
lang: context.user.language,
|
||||
defaultValue: text,
|
||||
});
|
||||
// Replace context tokens
|
||||
text = this.processTokenReplacements(text, context, settings);
|
||||
return text;
|
||||
|
@ -120,10 +120,6 @@ export const config: Config = {
|
||||
limit: 10,
|
||||
},
|
||||
chatbot: {
|
||||
lang: {
|
||||
default: 'en',
|
||||
available: ['en', 'fr'],
|
||||
},
|
||||
messages: {
|
||||
track_delivery: false,
|
||||
track_read: false,
|
||||
|
@ -15,7 +15,6 @@ type TJwtOptions = {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
};
|
||||
type TLanguage = 'en' | 'fr' | 'ar' | 'tn';
|
||||
type TMethods = 'GET' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD';
|
||||
type TLogLevel = 'log' | 'fatal' | 'error' | 'warn' | 'debug' | 'verbose';
|
||||
type TCacheConfig = {
|
||||
@ -87,10 +86,6 @@ export type Config = {
|
||||
limit: number;
|
||||
};
|
||||
chatbot: {
|
||||
lang: {
|
||||
default: TLanguage;
|
||||
available: TLanguage[];
|
||||
};
|
||||
messages: {
|
||||
track_delivery: boolean;
|
||||
track_read: boolean;
|
||||
|
@ -477,7 +477,7 @@ export default class OfflineHandler extends ChannelHandler {
|
||||
...channelData,
|
||||
name: this.getChannel(),
|
||||
},
|
||||
language: config.chatbot.lang.default,
|
||||
language: '',
|
||||
locale: '',
|
||||
timezone: 0,
|
||||
gender: 'male',
|
||||
|
@ -87,8 +87,6 @@ describe('NLP Default Helper', () => {
|
||||
provider: 'default',
|
||||
endpoint: 'path',
|
||||
token: 'token',
|
||||
languages: ['fr', 'ar', 'tn'],
|
||||
default_lang: 'fr',
|
||||
threshold: '0.5',
|
||||
},
|
||||
})),
|
||||
|
@ -51,8 +51,11 @@ import {
|
||||
|
||||
import { TranslationController } from './translation.controller';
|
||||
import { TranslationUpdateDto } from '../dto/translation.dto';
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { TranslationRepository } from '../repositories/translation.repository';
|
||||
import { LanguageModel } from '../schemas/language.schema';
|
||||
import { Translation, TranslationModel } from '../schemas/translation.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
describe('TranslationController', () => {
|
||||
@ -73,6 +76,7 @@ describe('TranslationController', () => {
|
||||
MenuModel,
|
||||
BlockModel,
|
||||
ContentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@ -117,7 +121,7 @@ describe('TranslationController', () => {
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
initDynamicTranslations: jest.fn(),
|
||||
refreshDynamicTranslations: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -129,6 +133,8 @@ describe('TranslationController', () => {
|
||||
},
|
||||
},
|
||||
LoggerService,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
],
|
||||
}).compile();
|
||||
translationService = module.get<TranslationService>(TranslationService);
|
||||
|
@ -14,9 +14,9 @@ import {
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
@ -31,12 +31,14 @@ import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { TranslationUpdateDto } from '../dto/translation.dto';
|
||||
import { Translation } from '../schemas/translation.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('translation')
|
||||
export class TranslationController extends BaseController<Translation> {
|
||||
constructor(
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly translationService: TranslationService,
|
||||
private readonly settingService: SettingService,
|
||||
private readonly logger: LoggerService,
|
||||
@ -103,40 +105,37 @@ export class TranslationController extends BaseController<Translation> {
|
||||
@CsrfCheck(true)
|
||||
@Post('refresh')
|
||||
async refresh(): Promise<any> {
|
||||
const settings = await this.settingService.getSettings();
|
||||
const languages = settings.nlp_settings.languages;
|
||||
const defaultTrans: Translation['translations'] = languages.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr] = '';
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const defaultTrans: Translation['translations'] = Object.keys(languages)
|
||||
.filter((lang) => lang !== defaultLanguage.code)
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr] = '';
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
// Scan Blocks
|
||||
return this.translationService
|
||||
.getAllBlockStrings()
|
||||
.then(async (strings: string[]) => {
|
||||
const settingStrings =
|
||||
await this.translationService.getSettingStrings();
|
||||
// Scan global settings
|
||||
strings = strings.concat(settingStrings);
|
||||
// Filter unique and not empty messages
|
||||
strings = strings.filter((str, pos) => {
|
||||
return str && strings.indexOf(str) == pos;
|
||||
});
|
||||
// Perform refresh
|
||||
const queue = strings.map((str) =>
|
||||
this.translationService.findOneOrCreate(
|
||||
{ str },
|
||||
{ str, translations: defaultTrans as any, translated: 100 },
|
||||
),
|
||||
);
|
||||
return Promise.all(queue).then(() => {
|
||||
// Purge non existing translations
|
||||
return this.translationService.deleteMany({
|
||||
str: { $nin: strings },
|
||||
});
|
||||
});
|
||||
});
|
||||
let strings = await this.translationService.getAllBlockStrings();
|
||||
const settingStrings = await this.translationService.getSettingStrings();
|
||||
// Scan global settings
|
||||
strings = strings.concat(settingStrings);
|
||||
// Filter unique and not empty messages
|
||||
strings = strings.filter((str, pos) => {
|
||||
return str && strings.indexOf(str) == pos;
|
||||
});
|
||||
// Perform refresh
|
||||
const queue = strings.map((str) =>
|
||||
this.translationService.findOneOrCreate(
|
||||
{ str },
|
||||
{ str, translations: defaultTrans },
|
||||
),
|
||||
);
|
||||
await Promise.all(queue);
|
||||
// Purge non existing translations
|
||||
return this.translationService.deleteMany({
|
||||
str: { $nin: strings },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class LanguageCreateDto {
|
||||
@ApiProperty({ description: 'Language Title', type: String })
|
||||
@ -22,15 +22,14 @@ export class LanguageCreateDto {
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
@ApiProperty({ description: 'Is Default Language ?', type: Boolean })
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
isDefault: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Whether Language is RTL', type: Boolean })
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
isRTL?: boolean;
|
||||
isRTL: boolean;
|
||||
}
|
||||
|
||||
export class LanguageUpdateDto extends PartialType(LanguageCreateDto) {}
|
||||
export class LanguageUpdateDto extends PartialType(LanguageCreateDto) {
|
||||
@ApiProperty({ description: 'Is Default Language ?', type: Boolean })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
@ -30,8 +30,9 @@ export class Language extends BaseSchema {
|
||||
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
isDefault: boolean;
|
||||
isDefault?: boolean;
|
||||
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
|
@ -11,6 +11,7 @@ import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Translation extends BaseSchema {
|
||||
@ -26,17 +27,12 @@ export class Translation extends BaseSchema {
|
||||
required: true,
|
||||
})
|
||||
translations: Record<string, string>;
|
||||
|
||||
@Prop({
|
||||
type: Number,
|
||||
})
|
||||
translated: number;
|
||||
}
|
||||
|
||||
export const TranslationModel: ModelDefinition = {
|
||||
export const TranslationModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: Translation.name,
|
||||
schema: SchemaFactory.createForClass(Translation),
|
||||
};
|
||||
});
|
||||
|
||||
export type TranslationDocument = THydratedDocument<Translation>;
|
||||
|
||||
|
@ -13,11 +13,11 @@ export const languageModels: LanguageCreateDto[] = [
|
||||
{
|
||||
title: 'English',
|
||||
code: 'en',
|
||||
isDefault: true,
|
||||
isRTL: false,
|
||||
},
|
||||
{
|
||||
title: 'Français',
|
||||
code: 'fr',
|
||||
isDefault: false,
|
||||
isRTL: false,
|
||||
},
|
||||
];
|
||||
|
@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import {
|
||||
I18nService as NativeI18nService,
|
||||
Path,
|
||||
@ -24,11 +23,7 @@ import { Translation } from '@/i18n/schemas/translation.schema';
|
||||
export class I18nService<
|
||||
K = Record<string, unknown>,
|
||||
> extends NativeI18nService<K> {
|
||||
private dynamicTranslations: Record<string, Record<string, string>> =
|
||||
config.chatbot.lang.available.reduce(
|
||||
(acc, curr) => ({ ...acc, [curr]: {} }),
|
||||
{},
|
||||
);
|
||||
private dynamicTranslations: Record<string, Record<string, string>> = {};
|
||||
|
||||
t<P extends Path<K> = any, R = PathValue<K, P>>(
|
||||
key: P,
|
||||
@ -40,17 +35,19 @@ export class I18nService<
|
||||
...options,
|
||||
};
|
||||
let { lang } = options;
|
||||
lang = lang ?? this.i18nOptions.fallbackLanguage;
|
||||
lang = this.resolveLanguage(lang);
|
||||
|
||||
// Translate block message, button text, ...
|
||||
if (lang in this.dynamicTranslations) {
|
||||
if (key in this.dynamicTranslations[lang]) {
|
||||
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
|
||||
R,
|
||||
string,
|
||||
R
|
||||
>;
|
||||
if (this.dynamicTranslations[lang][key]) {
|
||||
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
|
||||
R,
|
||||
string,
|
||||
R
|
||||
>;
|
||||
}
|
||||
return options.defaultValue as IfAnyOrNever<R, string, R>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,15 +56,13 @@ export class I18nService<
|
||||
return super.t<P, R>(key, options);
|
||||
}
|
||||
|
||||
@OnEvent('hook:i18n:refresh')
|
||||
initDynamicTranslations(translations: Translation[]) {
|
||||
refreshDynamicTranslations(translations: Translation[]) {
|
||||
this.dynamicTranslations = translations.reduce((acc, curr) => {
|
||||
const { str, translations } = curr;
|
||||
Object.entries(translations)
|
||||
.filter(([lang]) => lang in acc)
|
||||
.forEach(([lang, t]) => {
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
Object.entries(translations).forEach(([lang, t]) => {
|
||||
acc[lang] = acc[lang] || {};
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, this.dynamicTranslations);
|
||||
|
@ -33,7 +33,7 @@ export class TranslationService extends BaseService<Translation> {
|
||||
|
||||
public async resetI18nTranslations() {
|
||||
const translations = await this.findAll();
|
||||
this.i18n.initDynamicTranslations(translations);
|
||||
this.i18n.refreshDynamicTranslations(translations);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, Types } from 'mongoose';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { BaseRepository } from '@/utils/generics/base-repository';
|
||||
|
||||
@ -65,8 +64,7 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
* Emits an event after a `Setting` has been updated.
|
||||
*
|
||||
* This method is used to synchronize global settings by emitting an event
|
||||
* based on the `group` and `label` of the `Setting`. It also updates the i18n
|
||||
* default language setting when the `default_lang` label is updated.
|
||||
* based on the `group` and `label` of the `Setting`.
|
||||
*
|
||||
* @param _query The Mongoose query object used to find and update the document.
|
||||
* @param setting The updated `Setting` object.
|
||||
@ -86,33 +84,5 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
'hook:settings:' + setting.group + ':' + setting.label,
|
||||
setting,
|
||||
);
|
||||
|
||||
if (setting.label === 'default_lang') {
|
||||
// @todo : check if this actually updates the default lang
|
||||
this.i18n.resolveLanguage(setting.value as string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default values before creating a `Setting` document.
|
||||
*
|
||||
* If the setting is part of the `nlp_settings` group, it sets specific values
|
||||
* for `languages` and `default_lang` labels, using configuration values from the
|
||||
* chatbot settings.
|
||||
*
|
||||
* @param setting The `Setting` document to be created.
|
||||
*/
|
||||
async preCreate(
|
||||
setting: Document<unknown, unknown, Setting> &
|
||||
Setting & { _id: Types.ObjectId },
|
||||
) {
|
||||
if (setting.group === 'nlp_settings') {
|
||||
if (setting.label === 'languages') {
|
||||
setting.value = config.chatbot.lang.available;
|
||||
} else if (setting.label === 'default_lang') {
|
||||
setting.value = config.chatbot.lang.default;
|
||||
setting.options = config.chatbot.lang.available;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,8 +98,6 @@ export type SettingDict = { [group: string]: Setting[] };
|
||||
|
||||
export type Settings = {
|
||||
nlp_settings: {
|
||||
default_lang: string;
|
||||
languages: string[];
|
||||
threshold: string;
|
||||
provider: string;
|
||||
endpoint: string;
|
||||
|
@ -7,8 +7,6 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { config } from '@/config';
|
||||
|
||||
import { SettingCreateDto } from '../dto/setting.dto';
|
||||
import { SettingType } from '../schemas/types';
|
||||
|
||||
@ -67,26 +65,6 @@ export const settingModels: SettingCreateDto[] = [
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'languages',
|
||||
value: [],
|
||||
options: [],
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: true,
|
||||
allowCreate: true,
|
||||
},
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'default_lang',
|
||||
value: config.chatbot.lang.default,
|
||||
options: [], // NOTE : will be set onBeforeCreate from config
|
||||
type: SettingType.select,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'threshold',
|
||||
@ -97,7 +75,7 @@ export const settingModels: SettingCreateDto[] = [
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
weight: 6,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
|
@ -23,7 +23,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@ -69,6 +72,7 @@ describe('AuthController', () => {
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@ -86,6 +90,8 @@ describe('AuthController', () => {
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
InvitationService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
JwtService,
|
||||
{
|
||||
provide: MailerService,
|
||||
|
@ -20,7 +20,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { installPermissionFixtures } from '@/utils/test/fixtures/permission';
|
||||
@ -75,6 +78,7 @@ describe('UserController', () => {
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@ -108,6 +112,8 @@ describe('UserController', () => {
|
||||
},
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
ValidateAccountService,
|
||||
{
|
||||
provide: I18nService,
|
||||
|
@ -16,7 +16,10 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ISendMailOptions, MailerService } from '@nestjs-modules/mailer';
|
||||
import { SentMessageInfo } from 'nodemailer';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import {
|
||||
@ -55,6 +58,7 @@ describe('InvitationService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@ -66,6 +70,8 @@ describe('InvitationService', () => {
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
InvitationService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
JwtService,
|
||||
Logger,
|
||||
{
|
||||
|
@ -18,6 +18,7 @@ import { MailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
@ -42,6 +43,7 @@ export class InvitationService extends BaseService<
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
protected readonly i18n: I18nService,
|
||||
public readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
@ -63,6 +65,7 @@ export class InvitationService extends BaseService<
|
||||
const jwt = await this.sign(dto);
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'invitation.mjml',
|
||||
@ -70,7 +73,7 @@ export class InvitationService extends BaseService<
|
||||
token: jwt,
|
||||
// TODO: Which language should we use?
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('invitation_subject'),
|
||||
});
|
||||
|
@ -21,7 +21,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@ -52,6 +55,7 @@ describe('PasswordResetService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@ -62,6 +66,8 @@ describe('PasswordResetService', () => {
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
RoleRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
PasswordResetService,
|
||||
JwtService,
|
||||
|
@ -22,6 +22,7 @@ import { compareSync } from 'bcryptjs';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
@ -35,6 +36,7 @@ export class PasswordResetService {
|
||||
private logger: LoggerService,
|
||||
private readonly userService: UserService,
|
||||
public readonly i18n: I18nService,
|
||||
public readonly languageService: LanguageService,
|
||||
) {}
|
||||
|
||||
public readonly jwtSignOptions: JwtSignOptions = {
|
||||
@ -59,6 +61,7 @@ export class PasswordResetService {
|
||||
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'password_reset.mjml',
|
||||
@ -66,7 +69,7 @@ export class PasswordResetService {
|
||||
token: jwt,
|
||||
first_name: user.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('password_reset_subject'),
|
||||
});
|
||||
|
@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
@ -17,7 +18,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@ -46,6 +50,7 @@ describe('ValidateAccountService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@ -56,6 +61,8 @@ describe('ValidateAccountService', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
{
|
||||
provide: MailerService,
|
||||
@ -74,6 +81,14 @@ describe('ValidateAccountService', () => {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
validateAccountService = module.get<ValidateAccountService>(
|
||||
|
@ -19,6 +19,8 @@ import { MailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
import { UserCreateDto } from '../dto/user.dto';
|
||||
@ -35,7 +37,9 @@ export class ValidateAccountService {
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
private readonly userService: UserService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
private readonly i18n: I18nService,
|
||||
private readonly languageService: LanguageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -73,17 +77,28 @@ export class ValidateAccountService {
|
||||
const confirmationToken = await this.sign({ email: dto.email });
|
||||
|
||||
if (this.mailerService) {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
},
|
||||
subject: this.i18n.t('account_confirmation_subject'),
|
||||
});
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('account_confirmation_subject'),
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'Could not send email',
|
||||
e.message,
|
||||
e.stack,
|
||||
'ValidateAccount',
|
||||
);
|
||||
throw new InternalServerErrorException('Could not send email');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import AttachmentInput from "@/app-components/attachment/AttachmentInput";
|
||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { isSameEntity } from "@/hooks/crud/helpers";
|
||||
import { useApiClient } from "@/hooks/useApiClient";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
@ -40,12 +41,19 @@ export const NlpImportDialog: FC<NlpImportDialogProps> = ({
|
||||
attachmentId && (await apiClient.importNlpSamples(attachmentId));
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.removeQueries([
|
||||
QueryType.collection,
|
||||
EntityType.NLP_SAMPLE,
|
||||
]);
|
||||
queryClient.removeQueries([QueryType.count, EntityType.NLP_SAMPLE]);
|
||||
queryClient.removeQueries({
|
||||
predicate: ({ queryKey }) => {
|
||||
const [qType, qEntity] = queryKey;
|
||||
|
||||
return (
|
||||
((qType === QueryType.count || qType === QueryType.collection) &&
|
||||
isSameEntity(qEntity, EntityType.NLP_SAMPLE)) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_SAMPLE_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_VALUE)
|
||||
);
|
||||
},
|
||||
});
|
||||
handleCloseDialog();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
|
@ -149,6 +149,7 @@ export default function NlpSample() {
|
||||
renderCell: ({ row }) =>
|
||||
row.entities
|
||||
.map((e) => getSampleEntityFromCache(e) as INlpSampleEntity)
|
||||
.filter((e) => !!e)
|
||||
.map((entity) => (
|
||||
<ChipEntity
|
||||
id={entity.entity}
|
||||
|
@ -7,8 +7,14 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Dialog, DialogActions, DialogContent } from "@mui/material";
|
||||
import { useEffect, FC, useMemo } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
FormLabel,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { FC, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -16,15 +22,15 @@ import DialogButtons from "@/app-components/buttons/DialogButtons";
|
||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useSetting } from "@/hooks/useSetting";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { EntityType } from "@/services/types";
|
||||
import {
|
||||
ITranslation,
|
||||
ITranslationAttributes,
|
||||
ITranslations,
|
||||
ITranslation,
|
||||
} from "@/types/translation.types";
|
||||
|
||||
import TranslationInput from "./TranslationInput";
|
||||
@ -36,10 +42,14 @@ export const EditTranslationDialog: FC<EditTranslationDialogProps> = ({
|
||||
closeDialog,
|
||||
...rest
|
||||
}) => {
|
||||
const { data: languages } = useFind(
|
||||
{ entity: EntityType.LANGUAGE },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const availableLanguages = useSetting("nlp_settings", "languages");
|
||||
const defaultLanguage = useSetting("nlp_settings", "default_lang");
|
||||
const { mutateAsync: updateTranslation } = useUpdate(EntityType.TRANSLATION, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
@ -49,29 +59,16 @@ export const EditTranslationDialog: FC<EditTranslationDialogProps> = ({
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const defaultValues: ITranslation | undefined = useMemo(
|
||||
() =>
|
||||
data
|
||||
? {
|
||||
...data,
|
||||
translations: {
|
||||
...data?.translations,
|
||||
[defaultLanguage]: data?.str,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
[defaultLanguage, data],
|
||||
);
|
||||
const { reset, control, handleSubmit } = useForm<ITranslationAttributes>({
|
||||
defaultValues,
|
||||
defaultValues: data,
|
||||
});
|
||||
const onSubmitForm = async (params: ITranslationAttributes) => {
|
||||
if (data?.id) updateTranslation({ id: data.id, params });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) reset(defaultValues);
|
||||
}, [open, reset, defaultValues]);
|
||||
if (open) reset(data);
|
||||
}, [open, reset, data]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} fullWidth onClose={closeDialog} {...rest}>
|
||||
@ -80,21 +77,26 @@ export const EditTranslationDialog: FC<EditTranslationDialogProps> = ({
|
||||
{t("title.update_translation")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<ContentItem>
|
||||
<FormLabel>{t("label.original_text")}</FormLabel>
|
||||
<Typography component="p">{data?.str}</Typography>
|
||||
</ContentItem>
|
||||
<ContentContainer>
|
||||
{availableLanguages?.map((language: string) => (
|
||||
<ContentItem key={language}>
|
||||
<Controller
|
||||
name={`translations.${language as keyof ITranslations}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TranslationInput
|
||||
field={field}
|
||||
language={language as keyof ITranslations}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
{languages
|
||||
.filter(({ isDefault }) => !isDefault)
|
||||
.map((language) => (
|
||||
<ContentItem key={language.code}>
|
||||
<Controller
|
||||
name={`translations.${
|
||||
language.code as keyof ITranslations
|
||||
}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TranslationInput field={field} language={language} />
|
||||
)}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
</ContentContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
@ -7,24 +7,16 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { Grid } from "@mui/material";
|
||||
import React from "react";
|
||||
import { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import {
|
||||
ITranslationAttributes,
|
||||
ITranslations,
|
||||
} from "@/types/translation.types";
|
||||
|
||||
const isRTL = (language: string) => {
|
||||
return ["AR"].includes(language.toUpperCase());
|
||||
};
|
||||
import { ILanguage } from "@/types/language.types";
|
||||
import { ITranslationAttributes } from "@/types/translation.types";
|
||||
|
||||
interface RenderTranslationInputProps {
|
||||
language: keyof ITranslations;
|
||||
language: ILanguage;
|
||||
field: ControllerRenderProps<ITranslationAttributes, any>;
|
||||
}
|
||||
|
||||
@ -34,14 +26,14 @@ const TranslationInput: React.FC<RenderTranslationInputProps> = ({
|
||||
}) => (
|
||||
<Input
|
||||
inputRef={field.ref}
|
||||
dir={isRTL(language) ? "rtl" : "ltr"}
|
||||
dir={language.isRTL ? "rtl" : "ltr"}
|
||||
label={
|
||||
<Grid container dir="ltr">
|
||||
<Grid>{language.toUpperCase()}</Grid>
|
||||
<Grid>{field.value ? <CheckIcon /> : <CloseIcon />}</Grid>
|
||||
<Grid>{language.title}</Grid>
|
||||
</Grid>
|
||||
}
|
||||
multiline={true}
|
||||
minRows={3}
|
||||
{...field}
|
||||
/>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { faLanguage } from "@fortawesome/free-solid-svg-icons";
|
||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||
import { Button, Chip, Grid, Paper } from "@mui/material";
|
||||
import { Button, Chip, Grid, Paper, Stack } from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -25,10 +25,10 @@ import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useRefreshTranslations } from "@/hooks/entities/translation-hooks";
|
||||
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
import { useSetting } from "@/hooks/useSetting";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { PageHeader } from "@/layout/content/PageHeader";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { ILanguage } from "@/types/language.types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
import { ITranslation } from "@/types/translation.types";
|
||||
import { getDateTimeFormatter } from "@/utils/date";
|
||||
@ -38,7 +38,12 @@ import { EditTranslationDialog } from "./EditTranslationDialog";
|
||||
export const Translations = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const availableLanguages = useSetting("nlp_settings", "languages");
|
||||
const { data: languages } = useFind(
|
||||
{ entity: EntityType.LANGUAGE },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const editDialogCtl = useDialog<ITranslation>(false);
|
||||
const deleteDialogCtl = useDialog<string>(false);
|
||||
const { onSearch, searchPayload } = useSearch<ITranslation>({
|
||||
@ -92,22 +97,23 @@ export const Translations = () => {
|
||||
field: "translations",
|
||||
headerName: t("label.translations"),
|
||||
sortable: false,
|
||||
renderCell: (params) =>
|
||||
availableLanguages.map((language: string) => (
|
||||
<Chip
|
||||
key={language}
|
||||
variant={
|
||||
params.row.translations[language] ? "available" : "unavailable"
|
||||
}
|
||||
label={language.toUpperCase()}
|
||||
/>
|
||||
)),
|
||||
},
|
||||
{
|
||||
maxWidth: 127,
|
||||
field: "translated",
|
||||
resizable: false,
|
||||
headerName: t("label.translated"),
|
||||
renderCell: (params) => (
|
||||
<Stack direction="row" my={1} spacing={1}>
|
||||
{languages
|
||||
.filter(({ isDefault }) => !isDefault)
|
||||
.map((language: ILanguage) => (
|
||||
<Chip
|
||||
key={language.code}
|
||||
variant={
|
||||
params.row.translations[language.code]
|
||||
? "available"
|
||||
: "unavailable"
|
||||
}
|
||||
label={language.title}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
maxWidth: 140,
|
||||
@ -167,7 +173,6 @@ export const Translations = () => {
|
||||
deleteTranslation(deleteDialogCtl.data);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Grid item width="100%">
|
||||
<DataGrid {...dataGridProps} columns={columns} />
|
||||
</Grid>
|
||||
|
@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { RegexInput } from "@/app-components/inputs/RegexInput";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import {
|
||||
IBlockAttributes,
|
||||
@ -25,7 +26,8 @@ import {
|
||||
PayloadPattern,
|
||||
} from "@/types/block.types";
|
||||
import { IMenuItem } from "@/types/menu.types";
|
||||
import { INlpValueFull } from "@/types/nlp-value.types";
|
||||
import { INlpEntity } from "@/types/nlp-entity.types";
|
||||
import { INlpValue } from "@/types/nlp-value.types";
|
||||
|
||||
import { ContentPostbackInput } from "./ContentPostbackInput";
|
||||
import { PostbackInput } from "./PostbackInput";
|
||||
@ -64,6 +66,7 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<IBlockAttributes>();
|
||||
const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY);
|
||||
const [pattern, setPattern] = useState<Pattern>(value);
|
||||
const [patternType, setPatternType] = useState<PatternType>(getType(value));
|
||||
const types = [
|
||||
@ -140,7 +143,7 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
{patternType === "nlp" ? (
|
||||
<AutoCompleteEntitySelect<INlpValueFull, "value">
|
||||
<AutoCompleteEntitySelect<INlpValue, "value">
|
||||
value={(pattern as NlpPattern[]).map((v) =>
|
||||
"value" in v && v.value ? v.value : v.entity,
|
||||
)}
|
||||
@ -153,25 +156,31 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
multiple={true}
|
||||
onChange={(_e, data) => {
|
||||
setPattern(
|
||||
data.map((d) =>
|
||||
d.value === "any"
|
||||
data.map((d) => {
|
||||
const entity = getNlpEntityFromCache(d.entity) as INlpEntity;
|
||||
|
||||
return d.value === "any"
|
||||
? {
|
||||
match: "entity",
|
||||
entity: d.entity.name,
|
||||
entity: entity.name,
|
||||
}
|
||||
: {
|
||||
match: "value",
|
||||
entity: d.entity.name,
|
||||
entity: entity.name,
|
||||
value: d.value,
|
||||
},
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
return `${option.entity.name}=${option.value}`;
|
||||
const entity = getNlpEntityFromCache(option.entity) as INlpEntity;
|
||||
|
||||
return `${entity.name}=${option.value}`;
|
||||
}}
|
||||
groupBy={(option) => {
|
||||
return option.entity.name;
|
||||
const entity = getNlpEntityFromCache(option.entity) as INlpEntity;
|
||||
|
||||
return entity.name;
|
||||
}}
|
||||
renderGroup={(params) => (
|
||||
<li key={params.key}>
|
||||
@ -188,23 +197,25 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
)}
|
||||
preprocess={(options) => {
|
||||
return options.reduce((acc, curr) => {
|
||||
if (curr.entity.lookups.includes("keywords")) {
|
||||
const entity = getNlpEntityFromCache(curr.entity) as INlpEntity;
|
||||
|
||||
if (entity.lookups.includes("keywords")) {
|
||||
const exists = acc.find(
|
||||
({ value, id }) => value === "any" && id === curr.entity.id,
|
||||
({ value, id }) => value === "any" && id === entity.id,
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
acc.push({
|
||||
entity: curr.entity,
|
||||
id: curr.entity.id,
|
||||
entity: entity.id,
|
||||
id: entity.id,
|
||||
value: "any",
|
||||
} as INlpValueFull);
|
||||
} as INlpValue);
|
||||
}
|
||||
}
|
||||
acc.push(curr);
|
||||
|
||||
return acc;
|
||||
}, [] as INlpValueFull[]);
|
||||
}, [] as INlpValue[]);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -550,7 +550,8 @@
|
||||
"no_data": "No data",
|
||||
"code": "Code",
|
||||
"is_default": "Default",
|
||||
"is_rtl": "RTL"
|
||||
"is_rtl": "RTL",
|
||||
"original_text": "Original Text"
|
||||
},
|
||||
"placeholder": {
|
||||
"your_username": "Your username",
|
||||
|
@ -550,7 +550,8 @@
|
||||
"no_data": "Pas de données",
|
||||
"code": "Code",
|
||||
"is_default": "Par Défaut",
|
||||
"is_rtl": "RTL"
|
||||
"is_rtl": "RTL",
|
||||
"original_text": "Texte par défaut"
|
||||
},
|
||||
"placeholder": {
|
||||
"your_username": "Votre nom d'utilisateur",
|
||||
|
Loading…
Reference in New Issue
Block a user