feat: add language crud

This commit is contained in:
Mohamed Marrouchi 2024-09-22 12:51:56 +01:00
parent 614766c246
commit 10f36c2d48
25 changed files with 849 additions and 48 deletions

View File

@ -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<Language> {
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<Language>,
@Query(new SearchFilterPipe<Language>({ allowedFields: ['title', 'code'] }))
filters: TFilterQuery<Language>,
) {
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<Language>({
allowedFields: ['title', 'code'],
}),
)
filters?: TFilterQuery<Language>,
) {
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<Language> {
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<Language> {
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<Language> {
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<DeleteResult> {
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;
}
}

View File

@ -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) {}

View File

@ -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,

View File

@ -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<Language> {
constructor(@InjectModel(Language.name) readonly model: Model<Language>) {
super(model, Language);
}
}

View File

@ -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<Language>;
export default LanguageModel.schema;

View File

@ -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,
},
];

View File

@ -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<Language> {
constructor(private readonly languageRepository: LanguageRepository) {
super(languageRepository);
}
}

View File

@ -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<Language> {
constructor(readonly repository: LanguageRepository) {
super(repository);
}
}

View File

@ -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);

View File

@ -100,6 +100,11 @@ export const modelModels: ModelCreateDto[] = [
identity: 'subscriber',
attributes: {},
},
{
name: 'Language',
identity: 'language',
attributes: {},
},
{
name: 'Translation',
identity: 'translation',

View File

@ -26,6 +26,7 @@ export type TModel =
| 'conversation'
| 'message'
| 'subscriber'
| 'language'
| 'translation'
| 'botstats'
| 'menu'

View File

@ -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<T extends GridValidRowModel> {
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 <TocOutlinedIcon />;
case ActionColumnLabel.Manage_Labels:
return <LocalOfferIcon />;
case ActionColumnLabel.Toggle:
return <CheckCircle />;
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<T extends GridValidRowModel>({
return (
<Stack height="100%" alignItems="center" direction="row" spacing={0.5}>
{actions.map(({ label, action, requires = [] }) => (
<GridActionsCellItem
key={label}
className="actionButton"
icon={<Tooltip title={t(label)}>{getIcon(label)}</Tooltip>}
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,
}) => (
<GridActionsCellItem
key={label}
className="actionButton"
icon={
<Tooltip title={helperText || t(label)}>{getIcon(label)}</Tooltip>
}
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);
}}
/>
),
)}
</Stack>
);
}

View File

@ -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<ILanguage>;
export const LanguageDialog: FC<LanguageDialogProps> = ({
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<ILanguageAttributes>({
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 (
<Dialog open={open} fullWidth onClose={closeDialog} {...rest}>
<form onSubmit={handleSubmit(onSubmitForm)}>
<DialogTitle onClose={closeDialog}>
{data ? t("title.edit_label") : t("title.new_label")}
</DialogTitle>
<DialogContent>
<ContentContainer>
<ContentItem>
<Input
label={t("label.title")}
error={!!errors.title}
{...register("title", validationRules.title)}
helperText={errors.title ? errors.title.message : null}
multiline={true}
/>
</ContentItem>
<ContentItem>
<Input
label={t("label.code")}
error={!!errors.code}
{...register("code", validationRules.code)}
helperText={errors.code ? errors.code.message : null}
multiline={true}
/>
</ContentItem>
</ContentContainer>
</DialogContent>
<DialogActions>
<DialogButtons closeDialog={closeDialog} />
</DialogActions>
</form>
</Dialog>
);
};

View File

@ -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<ILanguage>(false);
const editDialogCtl = useDialog<ILanguage>(false);
const deleteDialogCtl = useDialog<string>(false);
const hasPermission = useHasPermission();
const { onSearch, searchPayload } = useSearch<ILanguage>({
$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<ILanguage>(
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<ILanguage>[] = [
{ 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 (
<Grid container gap={3} flexDirection="column">
<LanguageDialog {...getDisplayDialogs(addDialogCtl)} />
<LanguageDialog {...getDisplayDialogs(editDialogCtl)} />
<DeleteDialog
{...deleteDialogCtl}
callback={() => {
if (deleteDialogCtl?.data) deleteLanguage(deleteDialogCtl.data);
}}
/>
<PageHeader icon={Flag} title={t("title.languages")}>
<Grid
justifyContent="flex-end"
gap={1}
container
alignItems="center"
flexShrink={0}
width="max-content"
>
<Grid item>
<FilterTextfield onChange={onSearch} />
</Grid>
{hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? (
<Grid item>
<Button
startIcon={<AddIcon />}
variant="contained"
sx={{ float: "right" }}
onClick={() => addDialogCtl.openDialog()}
>
{t("button.add")}
</Button>
</Grid>
) : null}
</Grid>
</PageHeader>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<Grid>
<DataGrid columns={columns} {...dataGridProps} />
</Grid>
</Paper>
</Grid>
</Grid>
);
};

View File

@ -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"

View File

@ -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",

View File

@ -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",

View File

@ -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 <Languages />;
};
LanguagesPage.getLayout = function getLayout(page: ReactElement) {
return <Layout>{page}</Layout>;
};
export default LanguagesPage;

View File

@ -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",

View File

@ -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,

View File

@ -32,6 +32,7 @@ export enum EntityType {
NLP_VALUE = "NlpValue",
MESSAGE = "Message",
MENU = "Menu",
LANGUAGE = "Language",
TRANSLATION = "Translation",
ATTACHMENT = "Attachment",
CHANNEL = "Channel",

View File

@ -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<ILanguageAttributes, ILanguage>;
[EntityType.TRANSLATION]: IEntityTypes<ITranslationAttributes, ITranslation>;
[EntityType.USER]: IEntityTypes<IUserAttributes, IUser, IUserFull>;
[EntityType.ATTACHMENT]: IEntityTypes<IAttachmentAttributes, IAttachment>;

View File

@ -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<string, string>;
export interface ILanguageAttributes {
title: string;
code: string;
default: boolean;
}
export interface ILanguageStub
extends IBaseSchema,
OmitPopulate<ILanguageAttributes, EntityType.TRANSLATION> {}
export interface ILanguage extends ILanguageStub, IFormat<Format.BASIC> {}

View File

@ -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<string, string>;
@ -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<ITranslationAttributes, EntityType.TRANSLATION> {}
export interface ITranslation extends ITranslationStub, IFormat<Format.BASIC> {}