diff --git a/api/src/i18n/controllers/language.controller.ts b/api/src/i18n/controllers/language.controller.ts new file mode 100644 index 0000000..a8260a4 --- /dev/null +++ b/api/src/i18n/controllers/language.controller.ts @@ -0,0 +1,151 @@ +/* + * Copyright © 2024 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). + * 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 { + BadRequestException, + Body, + Controller, + Delete, + Get, + HttpCode, + NotFoundException, + Param, + Patch, + Post, + Query, + UseInterceptors, +} from '@nestjs/common'; +import { CsrfCheck } from '@tekuconcept/nestjs-csrf'; +import { TFilterQuery } from 'mongoose'; + +import { CsrfInterceptor } from '@/interceptors/csrf.interceptor'; +import { LoggerService } from '@/logger/logger.service'; +import { BaseController } from '@/utils/generics/base-controller'; +import { DeleteResult } from '@/utils/generics/base-repository'; +import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; +import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe'; +import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe'; + +import { LanguageCreateDto, LanguageUpdateDto } from '../dto/language.dto'; +import { Language } from '../schemas/language.schema'; +import { LanguageService } from '../services/language.service'; + +@UseInterceptors(CsrfInterceptor) +@Controller('language') +export class LanguageController extends BaseController { + constructor( + private readonly languageService: LanguageService, + private readonly logger: LoggerService, + ) { + super(languageService); + } + + /** + * Retrieves a paginated list of categories based on provided filters and pagination settings. + * @param pageQuery - The pagination settings. + * @param filters - The filters to apply to the language search. + * @returns A Promise that resolves to a paginated list of categories. + */ + @Get() + async findPage( + @Query(PageQueryPipe) pageQuery: PageQueryDto, + @Query(new SearchFilterPipe({ allowedFields: ['title', 'code'] })) + filters: TFilterQuery, + ) { + return await this.languageService.findPage(filters, pageQuery); + } + + /** + * Counts the filtered number of categories. + * @returns A promise that resolves to an object representing the filtered number of categories. + */ + @Get('count') + async filterCount( + @Query( + new SearchFilterPipe({ + allowedFields: ['title', 'code'], + }), + ) + filters?: TFilterQuery, + ) { + return await this.count(filters); + } + + /** + * Finds a language by its ID. + * @param id - The ID of the language to find. + * @returns A Promise that resolves to the found language. + */ + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const doc = await this.languageService.findOne(id); + if (!doc) { + this.logger.warn(`Unable to find Language by id ${id}`); + throw new NotFoundException(`Language with ID ${id} not found`); + } + return doc; + } + + /** + * Creates a new language. + * @param language - The data of the language to be created. + * @returns A Promise that resolves to the created language. + */ + @CsrfCheck(true) + @Post() + async create(@Body() language: LanguageCreateDto): Promise { + return await this.languageService.create(language); + } + + /** + * Updates an existing language. + * @param id - The ID of the language to be updated. + * @param languageUpdate - The updated data for the language. + * @returns A Promise that resolves to the updated language. + */ + @CsrfCheck(true) + @Patch(':id') + async updateOne( + @Param('id') id: string, + @Body() languageUpdate: LanguageUpdateDto, + ): Promise { + if ('default' in languageUpdate) { + if (languageUpdate.default) { + // A new default language is define, make sure that only one is marked as default + await this.languageService.updateMany({}, { default: false }); + } else { + throw new BadRequestException('Should not be able to disable default'); + } + } + + const result = await this.languageService.updateOne(id, languageUpdate); + if (!result) { + this.logger.warn(`Unable to update Language by id ${id}`); + throw new NotFoundException(`Language with ID ${id} not found`); + } + return result; + } + + /** + * Deletes a language by its ID. + * @param id - The ID of the language to be deleted. + * @returns A Promise that resolves to the deletion result. + */ + @CsrfCheck(true) + @Delete(':id') + @HttpCode(204) + async deleteOne(@Param('id') id: string): Promise { + const result = await this.languageService.deleteOne(id); + if (result.deletedCount === 0) { + this.logger.warn(`Unable to delete Language by id ${id}`); + throw new NotFoundException(`Language with ID ${id} not found`); + } + return result; + } +} diff --git a/api/src/i18n/dto/language.dto.ts b/api/src/i18n/dto/language.dto.ts new file mode 100644 index 0000000..6aede8f --- /dev/null +++ b/api/src/i18n/dto/language.dto.ts @@ -0,0 +1,31 @@ +/* + * Copyright © 2024 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). + * 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 { PartialType } from '@nestjs/mapped-types'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class LanguageCreateDto { + @ApiProperty({ description: 'Language Title', type: String }) + @IsNotEmpty() + @IsString() + title: string; + + @ApiProperty({ description: 'Language Code', type: String }) + @IsNotEmpty() + @IsString() + code: string; + + @ApiProperty({ description: 'Is Default Language ?', type: Boolean }) + @IsNotEmpty() + @IsBoolean() + default: boolean; +} + +export class LanguageUpdateDto extends PartialType(LanguageCreateDto) {} diff --git a/api/src/i18n/i18n.module.ts b/api/src/i18n/i18n.module.ts index 4117aa4..1f0a3fe 100644 --- a/api/src/i18n/i18n.module.ts +++ b/api/src/i18n/i18n.module.ts @@ -7,23 +7,36 @@ * 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 { DynamicModule, Global, Inject, Module } from '@nestjs/common'; +import { + DynamicModule, + forwardRef, + Global, + Inject, + Module, +} from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import { MongooseModule } from '@nestjs/mongoose'; import { I18N_OPTIONS, I18N_TRANSLATIONS, - I18nModule as NativeI18nModule, I18nOptions, I18nTranslation, + I18nModule as NativeI18nModule, } from 'nestjs-i18n'; import { Observable } from 'rxjs'; +import { ChatModule } from '@/chat/chat.module'; + +import { LanguageController } from './controllers/language.controller'; import { TranslationController } from './controllers/translation.controller'; +import { LanguageRepository } from './repositories/language.repository'; import { TranslationRepository } from './repositories/translation.repository'; +import { LanguageModel } from './schemas/language.schema'; import { TranslationModel } from './schemas/translation.schema'; +import { LanguageSeeder } from './seeds/language.seed'; import { TranslationSeeder } from './seeds/translation.seed'; import { I18nService } from './services/i18n.service'; +import { LanguageService } from './services/language.service'; import { TranslationService } from './services/translation.service'; @Global() @@ -43,10 +56,19 @@ export class I18nModule extends NativeI18nModule { const { imports, providers, controllers, exports } = super.forRoot(options); return { module: I18nModule, - imports: imports.concat([MongooseModule.forFeature([TranslationModel])]), - controllers: controllers.concat([TranslationController]), + imports: (imports || []).concat([ + MongooseModule.forFeature([LanguageModel, TranslationModel]), + forwardRef(() => ChatModule), + ]), + controllers: (controllers || []).concat([ + LanguageController, + TranslationController, + ]), providers: providers.concat([ I18nService, + LanguageRepository, + LanguageService, + LanguageSeeder, TranslationRepository, TranslationService, TranslationSeeder, diff --git a/api/src/i18n/repositories/language.repository.ts b/api/src/i18n/repositories/language.repository.ts new file mode 100644 index 0000000..0f3c455 --- /dev/null +++ b/api/src/i18n/repositories/language.repository.ts @@ -0,0 +1,23 @@ +/* + * Copyright © 2024 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). + * 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 { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; + +import { BaseRepository } from '@/utils/generics/base-repository'; + +import { Language } from '../schemas/language.schema'; + +@Injectable() +export class LanguageRepository extends BaseRepository { + constructor(@InjectModel(Language.name) readonly model: Model) { + super(model, Language); + } +} diff --git a/api/src/i18n/schemas/language.schema.ts b/api/src/i18n/schemas/language.schema.ts new file mode 100644 index 0000000..5ebdd61 --- /dev/null +++ b/api/src/i18n/schemas/language.schema.ts @@ -0,0 +1,44 @@ +/* + * Copyright © 2024 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). + * 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 { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose'; +import { THydratedDocument } from 'mongoose'; + +import { BaseSchema } from '@/utils/generics/base-schema'; + +@Schema({ timestamps: true }) +export class Language extends BaseSchema { + @Prop({ + type: String, + required: true, + unique: true, + }) + title: string; + + @Prop({ + type: String, + required: true, + unique: true, + }) + code: string; + + @Prop({ + type: Boolean, + }) + default: boolean; +} + +export const LanguageModel: ModelDefinition = { + name: Language.name, + schema: SchemaFactory.createForClass(Language), +}; + +export type LanguageDocument = THydratedDocument; + +export default LanguageModel.schema; diff --git a/api/src/i18n/seeds/language.seed-model.ts b/api/src/i18n/seeds/language.seed-model.ts new file mode 100644 index 0000000..602e9b2 --- /dev/null +++ b/api/src/i18n/seeds/language.seed-model.ts @@ -0,0 +1,23 @@ +/* + * Copyright © 2024 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). + * 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 { LanguageCreateDto } from '../dto/language.dto'; + +export const languageModels: LanguageCreateDto[] = [ + { + title: 'English', + code: 'en', + default: true, + }, + { + title: 'Français', + code: 'fr', + default: false, + }, +]; diff --git a/api/src/i18n/seeds/language.seed.ts b/api/src/i18n/seeds/language.seed.ts new file mode 100644 index 0000000..34a483d --- /dev/null +++ b/api/src/i18n/seeds/language.seed.ts @@ -0,0 +1,22 @@ +/* + * Copyright © 2024 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). + * 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 { Injectable } from '@nestjs/common'; + +import { BaseSeeder } from '@/utils/generics/base-seeder'; + +import { LanguageRepository } from '../repositories/language.repository'; +import { Language } from '../schemas/language.schema'; + +@Injectable() +export class LanguageSeeder extends BaseSeeder { + constructor(private readonly languageRepository: LanguageRepository) { + super(languageRepository); + } +} diff --git a/api/src/i18n/services/language.service.ts b/api/src/i18n/services/language.service.ts new file mode 100644 index 0000000..45547b2 --- /dev/null +++ b/api/src/i18n/services/language.service.ts @@ -0,0 +1,22 @@ +/* + * Copyright © 2024 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). + * 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 { Injectable } from '@nestjs/common'; + +import { BaseService } from '@/utils/generics/base-service'; + +import { LanguageRepository } from '../repositories/language.repository'; +import { Language } from '../schemas/language.schema'; + +@Injectable() +export class LanguageService extends BaseService { + constructor(readonly repository: LanguageRepository) { + super(repository); + } +} diff --git a/api/src/seeder.ts b/api/src/seeder.ts index a05b09d..19d7645 100644 --- a/api/src/seeder.ts +++ b/api/src/seeder.ts @@ -13,6 +13,8 @@ import { CategorySeeder } from './chat/seeds/category.seed'; import { categoryModels } from './chat/seeds/category.seed-model'; import { ContextVarSeeder } from './chat/seeds/context-var.seed'; import { contextVarModels } from './chat/seeds/context-var.seed-model'; +import { LanguageSeeder } from './i18n/seeds/language.seed'; +import { languageModels } from './i18n/seeds/language.seed-model'; import { TranslationSeeder } from './i18n/seeds/translation.seed'; import { translationModels } from './i18n/seeds/translation.seed-model'; import { LoggerService } from './logger/logger.service'; @@ -40,6 +42,7 @@ export async function seedDatabase(app: INestApplicationContext) { const settingSeeder = app.get(SettingSeeder); const permissionSeeder = app.get(PermissionSeeder); const userSeeder = app.get(UserSeeder); + const languageSeeder = app.get(LanguageSeeder); const translationSeeder = app.get(TranslationSeeder); const nlpEntitySeeder = app.get(NlpEntitySeeder); const nlpValueSeeder = app.get(NlpValueSeeder); @@ -127,6 +130,14 @@ export async function seedDatabase(app: INestApplicationContext) { throw e; } + // Seed languages + try { + await languageSeeder.seed(languageModels); + } catch (e) { + logger.error('Unable to seed the database with languages!'); + throw e; + } + // Seed translations try { await translationSeeder.seed(translationModels); diff --git a/api/src/user/seeds/model.seed-model.ts b/api/src/user/seeds/model.seed-model.ts index 076f27e..17b6d37 100644 --- a/api/src/user/seeds/model.seed-model.ts +++ b/api/src/user/seeds/model.seed-model.ts @@ -100,6 +100,11 @@ export const modelModels: ModelCreateDto[] = [ identity: 'subscriber', attributes: {}, }, + { + name: 'Language', + identity: 'language', + attributes: {}, + }, { name: 'Translation', identity: 'translation', diff --git a/api/src/user/types/model.type.ts b/api/src/user/types/model.type.ts index 55b62b6..ae45463 100644 --- a/api/src/user/types/model.type.ts +++ b/api/src/user/types/model.type.ts @@ -26,6 +26,7 @@ export type TModel = | 'conversation' | 'message' | 'subscriber' + | 'language' | 'translation' | 'botstats' | 'menu' diff --git a/frontend/src/app-components/tables/columns/getColumns.tsx b/frontend/src/app-components/tables/columns/getColumns.tsx index 52d9b02..a4127f4 100644 --- a/frontend/src/app-components/tables/columns/getColumns.tsx +++ b/frontend/src/app-components/tables/columns/getColumns.tsx @@ -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 { CheckCircle } from "@mui/icons-material"; import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettingsOutlined"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import EditIcon from "@mui/icons-material/EditOutlined"; @@ -39,12 +40,16 @@ export enum ActionColumnLabel { Content = "button.content", Fields = "button.fields", Manage_Labels = "title.manage_labels", + Toggle = "button.toggle", } export interface ActionColumn { label: ActionColumnLabel; action?: (row: T) => void; requires?: PermissionAction[]; + getState?: (row: T) => boolean; + helperText?: string; + isDisabled?: (row: T) => boolean; } const BUTTON_WIDTH = 60; @@ -70,6 +75,8 @@ function getIcon(label: ActionColumnLabel) { return ; case ActionColumnLabel.Manage_Labels: return ; + case ActionColumnLabel.Toggle: + return ; default: return <>; } @@ -78,7 +85,7 @@ function getIcon(label: ActionColumnLabel) { function getColor(label: ActionColumnLabel) { switch (label) { case ActionColumnLabel.Edit: - return theme.palette.warning.main; + return theme.palette.grey[900]; case ActionColumnLabel.Delete: return theme.palette.error.main; default: @@ -97,29 +104,46 @@ function StackComponent({ return ( - {actions.map(({ label, action, requires = [] }) => ( - {getIcon(label)}} - label={t(label)} - showInMenu={false} - sx={{ - color: "grey", - "&:hover": { - color: getColor(label), - }, - }} - disabled={ - params.row.builtin && - (requires.includes(PermissionAction.UPDATE) || - requires.includes(PermissionAction.DELETE)) - } - onClick={() => { - action && action(params.row); - }} - /> - ))} + {actions.map( + ({ + label, + action, + requires = [], + getState, + helperText, + isDisabled, + }) => ( + {getIcon(label)} + } + label={helperText || t(label)} + showInMenu={false} + sx={{ + color: + label === ActionColumnLabel.Toggle && + getState && + getState(params.row) + ? getColor(label) + : theme.palette.grey[600], + "&:hover": { + color: getColor(label), + }, + }} + disabled={ + (isDisabled && isDisabled(params.row)) || + (params.row.builtin && + (requires.includes(PermissionAction.UPDATE) || + requires.includes(PermissionAction.DELETE))) + } + onClick={() => { + action && action(params.row); + }} + /> + ), + )} ); } diff --git a/frontend/src/components/languages/LanguageDialog.tsx b/frontend/src/components/languages/LanguageDialog.tsx new file mode 100644 index 0000000..b260714 --- /dev/null +++ b/frontend/src/components/languages/LanguageDialog.tsx @@ -0,0 +1,130 @@ +/* + * Copyright © 2024 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). + * 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 { FC, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +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 { Input } from "@/app-components/inputs/Input"; +import { useCreate } from "@/hooks/crud/useCreate"; +import { useUpdate } from "@/hooks/crud/useUpdate"; +import { DialogControlProps } from "@/hooks/useDialog"; +import { useToast } from "@/hooks/useToast"; +import { EntityType } from "@/services/types"; +import { ILanguage, ILanguageAttributes } from "@/types/language.types"; + +export type LanguageDialogProps = DialogControlProps; +export const LanguageDialog: FC = ({ + open, + data, + closeDialog, + ...rest +}) => { + const { t } = useTranslation(); + const { toast } = useToast(); + const { mutateAsync: createLanguage } = useCreate(EntityType.LANGUAGE, { + onError: () => { + toast.error(t("message.internal_server_error")); + }, + onSuccess() { + closeDialog(); + toast.success(t("message.success_save")); + }, + }); + const { mutateAsync: updateLanguage } = useUpdate(EntityType.LANGUAGE, { + onError: () => { + toast.error(t("message.internal_server_error")); + }, + onSuccess() { + closeDialog(); + toast.success(t("message.success_save")); + }, + }); + const { + reset, + register, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: { + title: data?.title || "", + code: data?.code || "", + }, + }); + const validationRules = { + title: { + required: t("message.title_is_required"), + }, + code: { + required: t("message.code_is_required"), + }, + }; + const onSubmitForm = async (params: ILanguageAttributes) => { + if (data) { + updateLanguage({ id: data.id, params }); + } else { + createLanguage(params); + } + }; + + useEffect(() => { + if (open) reset(); + }, [open, reset]); + + useEffect(() => { + if (data) { + reset({ + title: data.title, + code: data.code, + }); + } else { + reset(); + } + }, [data, reset]); + + return ( + +
+ + {data ? t("title.edit_label") : t("title.new_label")} + + + + + + + + + + + + + + +
+
+ ); +}; diff --git a/frontend/src/components/languages/index.tsx b/frontend/src/components/languages/index.tsx new file mode 100644 index 0000000..e8ddcf0 --- /dev/null +++ b/frontend/src/components/languages/index.tsx @@ -0,0 +1,204 @@ +/* + * Copyright © 2024 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). + * 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 { Flag } from "@mui/icons-material"; +import AddIcon from "@mui/icons-material/Add"; +import { Button, Grid, Paper } from "@mui/material"; +import { GridColDef } from "@mui/x-data-grid"; +import { useTranslation } from "react-i18next"; + +import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog"; +import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; +import { + ActionColumnLabel, + useActionColumns, +} from "@/app-components/tables/columns/getColumns"; +import { renderHeader } from "@/app-components/tables/columns/renderHeader"; +import { DataGrid } from "@/app-components/tables/DataGrid"; +import { useDelete } from "@/hooks/crud/useDelete"; +import { useFind } from "@/hooks/crud/useFind"; +import { useUpdate } from "@/hooks/crud/useUpdate"; +import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +import { useHasPermission } from "@/hooks/useHasPermission"; +import { useSearch } from "@/hooks/useSearch"; +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 { getDateTimeFormatter } from "@/utils/date"; + +import { LanguageDialog } from "./LanguageDialog"; + +export const Languages = () => { + const { t } = useTranslation(); + const { toast } = useToast(); + const addDialogCtl = useDialog(false); + const editDialogCtl = useDialog(false); + const deleteDialogCtl = useDialog(false); + const hasPermission = useHasPermission(); + const { onSearch, searchPayload } = useSearch({ + $or: ["title", "code"], + }); + const { dataGridProps, refetch } = useFind( + { entity: EntityType.LANGUAGE }, + { + params: searchPayload, + }, + ); + const { mutateAsync: updateLanguage } = useUpdate(EntityType.LANGUAGE, { + onError: () => { + toast.error(t("message.internal_server_error")); + }, + onSuccess() { + refetch(); + toast.success(t("message.success_save")); + }, + }); + const { mutateAsync: deleteLanguage } = useDelete(EntityType.LANGUAGE, { + onError: () => { + toast.error(t("message.internal_server_error")); + }, + onSuccess() { + deleteDialogCtl.closeDialog(); + toast.success(t("message.item_delete_success")); + }, + }); + const toggleDefault = (row: ILanguage) => { + if (!row.default) { + updateLanguage({ + id: row.id, + params: { + default: true, + }, + }); + } + }; + const actionColumns = useActionColumns( + EntityType.LANGUAGE, + [ + { + label: ActionColumnLabel.Toggle, + action: (row) => toggleDefault(row), + requires: [PermissionAction.UPDATE], + getState: (row) => row.default, + helperText: t("button.mark_as_default"), + }, + { + label: ActionColumnLabel.Edit, + action: (row) => editDialogCtl.openDialog(row), + requires: [PermissionAction.UPDATE], + }, + { + label: ActionColumnLabel.Delete, + action: (row) => deleteDialogCtl.openDialog(row.id), + requires: [PermissionAction.DELETE], + isDisabled: (row) => row.default, + }, + ], + t("label.operations"), + ); + const columns: GridColDef[] = [ + { field: "id", headerName: "ID" }, + { + flex: 2, + field: "title", + headerName: t("label.title"), + disableColumnMenu: true, + renderHeader, + headerAlign: "left", + }, + { + flex: 1, + field: "code", + headerName: t("label.code"), + disableColumnMenu: true, + renderHeader, + headerAlign: "left", + }, + { + flex: 1, + field: "default", + headerName: t("label.default"), + disableColumnMenu: true, + renderHeader, + headerAlign: "left", + valueGetter: (value) => (value ? t("label.yes") : t("label.no")), + }, + { + minWidth: 140, + field: "createdAt", + headerName: t("label.createdAt"), + disableColumnMenu: true, + renderHeader, + resizable: false, + headerAlign: "left", + valueGetter: (params) => + t("datetime.created_at", getDateTimeFormatter(params)), + }, + { + minWidth: 140, + field: "updatedAt", + headerName: t("label.updatedAt"), + disableColumnMenu: true, + renderHeader, + resizable: false, + headerAlign: "left", + valueGetter: (params) => + t("datetime.updated_at", getDateTimeFormatter(params)), + }, + actionColumns, + ]; + + return ( + + + + { + if (deleteDialogCtl?.data) deleteLanguage(deleteDialogCtl.data); + }} + /> + + + + + + {hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? ( + + + + ) : null} + + + + + + + + + + + ); +}; diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index f5a0dd0..f61ff19 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -122,6 +122,8 @@ "cms": "CMS", "nodes": "Content", "entities": "Content types", + "languages": "Languages", + "manage_localization": "Manage Localization", "translations": "Translations", "import": "Bulk Import", "media_library": "Media Library", @@ -183,6 +185,7 @@ "edit_node": "Edit Content", "import": "Bulk Import", "media_library": "Media Library", + "languages": "Languages", "translations": "Translations", "update_translation": "Update Translation", "broadcast": "Broadcast", @@ -544,7 +547,9 @@ "total": "Total", "general": "General", "other": "Other", - "no_data": "No data" + "no_data": "No data", + "code": "Code", + "default": "Default" }, "placeholder": { "your_username": "Your username", @@ -642,7 +647,8 @@ "media_library": "Media Library", "manage_roles": "Manage Roles", "connect_with_sso": "Connect with SSO", - "add_pattern": "Add pattern" + "add_pattern": "Add pattern", + "mark_as_default": "Mark as Default" }, "input": { "search": "Search" diff --git a/frontend/src/i18n/fr/translation.json b/frontend/src/i18n/fr/translation.json index 180dddb..1734cb1 100644 --- a/frontend/src/i18n/fr/translation.json +++ b/frontend/src/i18n/fr/translation.json @@ -123,6 +123,8 @@ "cms": "CMS", "nodes": "Contenu", "entities": "Types de contenu", + "manage_localization": "Internationalisation", + "languages": "Langues", "translations": "Traductions", "import": "Importation en masse", "media_library": "Bibliothéque Media", @@ -184,6 +186,7 @@ "edit_node": "Modifier le contenu", "import": "Importation en masse", "media_library": "Bibliothéque Media", + "languages": "Langues", "translations": "Traductions", "update_translation": "Mettre à jour la traduction", "broadcast": "Diffusion", @@ -544,7 +547,9 @@ "total": "Totale", "general": "Général", "other": "Autre", - "no_data": "Pas de données" + "no_data": "Pas de données", + "code": "Code", + "default": "Par Défaut" }, "placeholder": { "your_username": "Votre nom d'utilisateur", @@ -578,7 +583,8 @@ "start_date": "Date de début", "end_date": "Date de fin", "nlp_value": "Valeur", - "type_message_here": "Ecrivez quelque chose ici ...." + "type_message_here": "Ecrivez quelque chose ici ....", + "mark_as_default": "Par Défaut" }, "button": { "login": "Se connecter", diff --git a/frontend/src/layout/VerticalMenu.tsx b/frontend/src/layout/VerticalMenu.tsx index fe699d0..7344232 100644 --- a/frontend/src/layout/VerticalMenu.tsx +++ b/frontend/src/layout/VerticalMenu.tsx @@ -21,6 +21,7 @@ import { faUsers, IconDefinition, } from "@fortawesome/free-solid-svg-icons"; +import { Flag, Language } from "@mui/icons-material"; import AppsIcon from "@mui/icons-material/Apps"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import DriveFolderUploadIcon from "@mui/icons-material/DriveFolderUpload"; @@ -175,14 +176,6 @@ const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [ [EntityType.CONTENT_TYPE]: [PermissionAction.READ], }, }, - { - text: "menu.translations", - href: "/translations", - Icon: faLanguage, - requires: { - [EntityType.TRANSLATION]: [PermissionAction.READ], - }, - }, { text: "menu.media_library", href: "/content/media-library", @@ -249,6 +242,28 @@ const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [ : []), ], }, + { + text: "menu.manage_localization", + Icon: Language, + submenuItems: [ + { + text: "menu.languages", + href: "/localization/languages", + Icon: Flag, + requires: { + [EntityType.LANGUAGE]: [PermissionAction.READ], + }, + }, + { + text: "menu.translations", + href: "/localization/translations", + Icon: faLanguage, + requires: { + [EntityType.TRANSLATION]: [PermissionAction.READ], + }, + }, + ], + }, { text: "menu.settings", href: "/settings", diff --git a/frontend/src/pages/localization/languages.tsx b/frontend/src/pages/localization/languages.tsx new file mode 100644 index 0000000..d75c138 --- /dev/null +++ b/frontend/src/pages/localization/languages.tsx @@ -0,0 +1,23 @@ +/* + * Copyright © 2024 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). + * 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 { ReactElement } from "react"; + +import { Languages } from "@/components/languages"; +import { Layout } from "@/layout"; + +const LanguagesPage = () => { + return ; +}; + +LanguagesPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default LanguagesPage; diff --git a/frontend/src/pages/translations.tsx b/frontend/src/pages/localization/translations.tsx similarity index 100% rename from frontend/src/pages/translations.tsx rename to frontend/src/pages/localization/translations.tsx diff --git a/frontend/src/services/api.class.ts b/frontend/src/services/api.class.ts index c5bdd76..905d71b 100644 --- a/frontend/src/services/api.class.ts +++ b/frontend/src/services/api.class.ts @@ -59,6 +59,7 @@ export const ROUTES = { [EntityType.NLP_VALUE]: "/nlpvalue", [EntityType.NLP_SAMPLE_ENTITY]: "", [EntityType.MESSAGE]: "/message", + [EntityType.LANGUAGE]: "/language", [EntityType.TRANSLATION]: "/translation", [EntityType.ATTACHMENT]: "/attachment", [EntityType.CHANNEL]: "/channel", diff --git a/frontend/src/services/entities.ts b/frontend/src/services/entities.ts index 59bd692..f3bbc18 100644 --- a/frontend/src/services/entities.ts +++ b/frontend/src/services/entities.ts @@ -210,6 +210,15 @@ export const NlpSampleEntityEntity = new schema.Entity( }, ); +export const LanguageEntity = new schema.Entity( + EntityType.LANGUAGE, + undefined, + { + idAttribute: ({ id }) => id, + processStrategy: processCommonStrategy, + }, +); + export const TranslationEntity = new schema.Entity( EntityType.TRANSLATION, undefined, @@ -280,6 +289,7 @@ export const ENTITY_MAP = { [EntityType.NLP_ENTITY]: NlpEntityEntity, [EntityType.NLP_SAMPLE_ENTITY]: NlpSampleEntityEntity, [EntityType.NLP_VALUE]: NlpValueEntity, + [EntityType.LANGUAGE]: LanguageEntity, [EntityType.TRANSLATION]: TranslationEntity, [EntityType.ATTACHMENT]: AttachmentEntity, [EntityType.BLOCK]: BlockEntity, diff --git a/frontend/src/services/types.ts b/frontend/src/services/types.ts index 62f3776..49e8d47 100644 --- a/frontend/src/services/types.ts +++ b/frontend/src/services/types.ts @@ -32,6 +32,7 @@ export enum EntityType { NLP_VALUE = "NlpValue", MESSAGE = "Message", MENU = "Menu", + LANGUAGE = "Language", TRANSLATION = "Translation", ATTACHMENT = "Attachment", CHANNEL = "Channel", diff --git a/frontend/src/types/base.types.ts b/frontend/src/types/base.types.ts index 678898f..7ca8949 100644 --- a/frontend/src/types/base.types.ts +++ b/frontend/src/types/base.types.ts @@ -24,6 +24,7 @@ import { IContentType, IContentTypeAttributes } from "./content-type.types"; import { IContent, IContentAttributes, IContentFull } from "./content.types"; import { IContextVar, IContextVarAttributes } from "./context-var.types"; import { ILabel, ILabelAttributes, ILabelFull } from "./label.types"; +import { ILanguage, ILanguageAttributes } from "./language.types"; import { IMenuNode, IMenuNodeAttributes, @@ -106,6 +107,7 @@ export const POPULATE_BY_TYPE = { [EntityType.MESSAGE]: ["sender", "recipient", "sentBy"], [EntityType.MENU]: ["parent"], [EntityType.MENUTREE]: [], + [EntityType.LANGUAGE]: [], [EntityType.TRANSLATION]: [], [EntityType.ATTACHMENT]: [], [EntityType.CUSTOM_BLOCK]: [], @@ -189,6 +191,7 @@ export interface IEntityMapTypes { ISubscriber, ISubscriberFull >; + [EntityType.LANGUAGE]: IEntityTypes; [EntityType.TRANSLATION]: IEntityTypes; [EntityType.USER]: IEntityTypes; [EntityType.ATTACHMENT]: IEntityTypes; diff --git a/frontend/src/types/language.types.ts b/frontend/src/types/language.types.ts new file mode 100644 index 0000000..8d873e6 --- /dev/null +++ b/frontend/src/types/language.types.ts @@ -0,0 +1,26 @@ +/* + * Copyright © 2024 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). + * 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 { EntityType, Format } from "@/services/types"; + +import { IBaseSchema, IFormat, OmitPopulate } from "./base.types"; + +export type ILanguages = Record; + +export interface ILanguageAttributes { + title: string; + code: string; + default: boolean; +} + +export interface ILanguageStub + extends IBaseSchema, + OmitPopulate {} + +export interface ILanguage extends ILanguageStub, IFormat {} diff --git a/frontend/src/types/translation.types.ts b/frontend/src/types/translation.types.ts index ae9dcc3..a99d245 100644 --- a/frontend/src/types/translation.types.ts +++ b/frontend/src/types/translation.types.ts @@ -7,9 +7,9 @@ * 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 { Format } from "@/services/types"; +import { EntityType, Format } from "@/services/types"; -import { IBaseSchema, IFormat } from "./base.types"; +import { IBaseSchema, IFormat, OmitPopulate } from "./base.types"; export type ITranslations = Record; @@ -19,11 +19,8 @@ export interface ITranslationAttributes { translated: number; } -export interface ITranslationStub extends IBaseSchema { - str: string; - translations: ITranslations; - translated: number; -} +export interface ITranslationStub + extends IBaseSchema, + OmitPopulate {} export interface ITranslation extends ITranslationStub, IFormat {} -