mirror of
https://github.com/hexastack/hexabot
synced 2025-05-11 16:10:53 +00:00
Merge pull request #133 from Hexastack/fix/update-content-issue
fix: content update
This commit is contained in:
commit
5df35d94f7
@ -6,13 +6,7 @@
|
|||||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
forwardRef,
|
|
||||||
MiddlewareConsumer,
|
|
||||||
Module,
|
|
||||||
NestModule,
|
|
||||||
RequestMethod,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { MongooseModule } from '@nestjs/mongoose';
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
|
||||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||||
@ -21,7 +15,6 @@ import { ChatModule } from '@/chat/chat.module';
|
|||||||
import { ContentTypeController } from './controllers/content-type.controller';
|
import { ContentTypeController } from './controllers/content-type.controller';
|
||||||
import { ContentController } from './controllers/content.controller';
|
import { ContentController } from './controllers/content.controller';
|
||||||
import { MenuController } from './controllers/menu.controller';
|
import { MenuController } from './controllers/menu.controller';
|
||||||
import { ContentMiddleWare } from './middlewares/content.middleware';
|
|
||||||
import { ContentTypeRepository } from './repositories/content-type.repository';
|
import { ContentTypeRepository } from './repositories/content-type.repository';
|
||||||
import { ContentRepository } from './repositories/content.repository';
|
import { ContentRepository } from './repositories/content.repository';
|
||||||
import { MenuRepository } from './repositories/menu.repository';
|
import { MenuRepository } from './repositories/menu.repository';
|
||||||
@ -55,13 +48,4 @@ import { AttachmentModel } from '../attachment/schemas/attachment.schema';
|
|||||||
],
|
],
|
||||||
exports: [MenuService, ContentService, ContentTypeService],
|
exports: [MenuService, ContentService, ContentTypeService],
|
||||||
})
|
})
|
||||||
export class CmsModule implements NestModule {
|
export class CmsModule {}
|
||||||
configure(consumer: MiddlewareConsumer) {
|
|
||||||
consumer
|
|
||||||
.apply(ContentMiddleWare)
|
|
||||||
.forRoutes(
|
|
||||||
{ path: 'content', method: RequestMethod.POST },
|
|
||||||
{ path: 'content/:id', method: RequestMethod.PATCH },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -35,7 +35,6 @@ import {
|
|||||||
|
|
||||||
import { ContentController } from './content.controller';
|
import { ContentController } from './content.controller';
|
||||||
import { ContentCreateDto } from '../dto/content.dto';
|
import { ContentCreateDto } from '../dto/content.dto';
|
||||||
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
|
|
||||||
import { ContentTypeRepository } from '../repositories/content-type.repository';
|
import { ContentTypeRepository } from '../repositories/content-type.repository';
|
||||||
import { ContentRepository } from '../repositories/content.repository';
|
import { ContentRepository } from '../repositories/content.repository';
|
||||||
import { ContentType, ContentTypeModel } from '../schemas/content-type.schema';
|
import { ContentType, ContentTypeModel } from '../schemas/content-type.schema';
|
||||||
@ -48,7 +47,6 @@ describe('ContentController', () => {
|
|||||||
let contentService: ContentService;
|
let contentService: ContentService;
|
||||||
let contentTypeService: ContentTypeService;
|
let contentTypeService: ContentTypeService;
|
||||||
let attachmentService: AttachmentService;
|
let attachmentService: AttachmentService;
|
||||||
let transformInterceptor: ContentTransformInterceptor;
|
|
||||||
let contentType: ContentType;
|
let contentType: ContentType;
|
||||||
let content: Content;
|
let content: Content;
|
||||||
let attachment: Attachment;
|
let attachment: Attachment;
|
||||||
@ -74,7 +72,6 @@ describe('ContentController', () => {
|
|||||||
AttachmentService,
|
AttachmentService,
|
||||||
ContentTypeRepository,
|
ContentTypeRepository,
|
||||||
AttachmentRepository,
|
AttachmentRepository,
|
||||||
ContentTransformInterceptor,
|
|
||||||
EventEmitter2,
|
EventEmitter2,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
@ -82,9 +79,6 @@ describe('ContentController', () => {
|
|||||||
contentService = module.get<ContentService>(ContentService);
|
contentService = module.get<ContentService>(ContentService);
|
||||||
attachmentService = module.get<AttachmentService>(AttachmentService);
|
attachmentService = module.get<AttachmentService>(AttachmentService);
|
||||||
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
|
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
|
||||||
transformInterceptor = module.get<ContentTransformInterceptor>(
|
|
||||||
ContentTransformInterceptor,
|
|
||||||
);
|
|
||||||
contentType = await contentTypeService.findOne({ name: 'Product' });
|
contentType = await contentTypeService.findOne({ name: 'Product' });
|
||||||
content = await contentService.findOne({
|
content = await contentService.findOne({
|
||||||
title: 'Jean',
|
title: 'Jean',
|
||||||
@ -237,28 +231,6 @@ describe('ContentController', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filterDynamicFields', () => {
|
|
||||||
it('should flatten dynamic fields', () => {
|
|
||||||
const result = transformInterceptor.transformDynamicFields(
|
|
||||||
contentFixtures[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualPayload(
|
|
||||||
{
|
|
||||||
title: 'Jean',
|
|
||||||
status: true,
|
|
||||||
subtitle: 'Jean Droit Taille Normale',
|
|
||||||
image: {
|
|
||||||
payload: {
|
|
||||||
url: 'https://images-na.ssl-images-amazon.com/images/I/31DY09uzLDL._SX38_SY50_CR,0,0,38,50_.jpg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
['entity', 'rag', ...IGNORED_TEST_FIELDS],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('count', () => {
|
describe('count', () => {
|
||||||
it('should return the number of contents', async () => {
|
it('should return the number of contents', async () => {
|
||||||
jest.spyOn(contentService, 'count');
|
jest.spyOn(contentService, 'count');
|
||||||
|
@ -39,8 +39,7 @@ import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
|||||||
|
|
||||||
import { ContentTypeService } from './../services/content-type.service';
|
import { ContentTypeService } from './../services/content-type.service';
|
||||||
import { ContentService } from './../services/content.service';
|
import { ContentService } from './../services/content.service';
|
||||||
import { ContentCreateDto } from '../dto/content.dto';
|
import { ContentCreateDto, ContentUpdateDto } from '../dto/content.dto';
|
||||||
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
|
|
||||||
import { ContentType } from '../schemas/content-type.schema';
|
import { ContentType } from '../schemas/content-type.schema';
|
||||||
import {
|
import {
|
||||||
Content,
|
Content,
|
||||||
@ -48,9 +47,8 @@ import {
|
|||||||
ContentPopulate,
|
ContentPopulate,
|
||||||
ContentStub,
|
ContentStub,
|
||||||
} from '../schemas/content.schema';
|
} from '../schemas/content.schema';
|
||||||
import { preprocessDynamicFields } from '../utilities';
|
|
||||||
|
|
||||||
@UseInterceptors(ContentTransformInterceptor, CsrfInterceptor)
|
@UseInterceptors(CsrfInterceptor)
|
||||||
@Controller('content')
|
@Controller('content')
|
||||||
export class ContentController extends BaseController<
|
export class ContentController extends BaseController<
|
||||||
Content,
|
Content,
|
||||||
@ -116,8 +114,7 @@ export class ContentController extends BaseController<
|
|||||||
entity: contentType?.id,
|
entity: contentType?.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const newContent = this.filterDynamicFields(contentDto, contentType);
|
return await this.contentService.create(contentDto);
|
||||||
return await this.contentService.create(newContent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,12 +183,22 @@ export class ContentController extends BaseController<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentsDto = result.data.map((content) => {
|
const contentsDto = result.data.reduce(
|
||||||
content.entity = targetContentType;
|
(acc, { title, status, ...rest }) => [
|
||||||
const dto = preprocessDynamicFields(content);
|
...acc,
|
||||||
// Match headers against entity fields
|
{
|
||||||
return this.filterDynamicFields(dto, contentType);
|
title,
|
||||||
});
|
status,
|
||||||
|
entity: targetContentType,
|
||||||
|
dynamicFields: Object.keys(rest)
|
||||||
|
.filter((key) =>
|
||||||
|
contentType.fields.map((field) => field.name).includes(key),
|
||||||
|
)
|
||||||
|
.reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// Create content
|
// Create content
|
||||||
return await this.contentService.createMany(contentsDto);
|
return await this.contentService.createMany(contentsDto);
|
||||||
@ -319,16 +326,12 @@ export class ContentController extends BaseController<
|
|||||||
* @returns The updated content document.
|
* @returns The updated content document.
|
||||||
*/
|
*/
|
||||||
@CsrfCheck(true)
|
@CsrfCheck(true)
|
||||||
@Patch('/:id')
|
@Patch(':id')
|
||||||
async updateOne(
|
async updateOne(
|
||||||
@Body() contentDto: ContentCreateDto,
|
@Body() contentDto: ContentUpdateDto,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
): Promise<Content> {
|
): Promise<Content> {
|
||||||
const contentType = await this.contentTypeService.findOne(
|
const updatedContent = await this.contentService.updateOne(id, contentDto);
|
||||||
contentDto.entity,
|
|
||||||
);
|
|
||||||
const newContent = this.filterDynamicFields(contentDto, contentType);
|
|
||||||
const updatedContent = await this.contentService.updateOne(id, newContent);
|
|
||||||
if (!updatedContent) {
|
if (!updatedContent) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Failed to update content with id ${id}. Content not found.`,
|
`Failed to update content with id ${id}. Content not found.`,
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';
|
||||||
import { IsString, IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
|
import { IsString, IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
||||||
@ -32,3 +32,5 @@ export class ContentCreateDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
dynamicFields?: Record<string, any>;
|
dynamicFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ContentUpdateDto extends PartialType(ContentCreateDto) {}
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
Injectable,
|
|
||||||
NestInterceptor,
|
|
||||||
ExecutionContext,
|
|
||||||
CallHandler,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Observable, map } from 'rxjs';
|
|
||||||
|
|
||||||
import { Content } from '../schemas/content.schema';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ContentTransformInterceptor
|
|
||||||
implements NestInterceptor<Content, Content>
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
-This interceptor is designed to provide a flattened representation of the 'dynamicFields'.
|
|
||||||
-The incoming data contains a 'dynamicField' object, and the interceptor is expanding it,
|
|
||||||
extracting its content as separate entries.
|
|
||||||
-After the expansion, the 'dynamicFields' property is removed.
|
|
||||||
-The interceptor will be applied on each endpoint of this controller.
|
|
||||||
*/
|
|
||||||
transformDynamicFields(data) {
|
|
||||||
if (data.dynamicFields) {
|
|
||||||
Object.keys(data.dynamicFields).forEach((key) => {
|
|
||||||
data[key] = data.dynamicFields[key];
|
|
||||||
});
|
|
||||||
delete data.dynamicFields;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<Content> {
|
|
||||||
return next.handle().pipe(
|
|
||||||
map((data) => {
|
|
||||||
// If the data is not an array, the 'transformDynamicFields' method is applied once
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
return this.transformDynamicFields(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.map((content) => {
|
|
||||||
return this.transformDynamicFields(content);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
||||||
import { NextFunction, Request, Response } from 'express';
|
|
||||||
|
|
||||||
import { preprocessDynamicFields } from '../utilities';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ContentMiddleWare implements NestMiddleware {
|
|
||||||
use(req: Request, _res: Response, next: NextFunction) {
|
|
||||||
req.body = preprocessDynamicFields(req.body);
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ContentCreateDto } from '../dto/content.dto';
|
|
||||||
|
|
||||||
export const preprocessDynamicFields = (
|
|
||||||
content: Record<string, string | boolean | number>,
|
|
||||||
) => {
|
|
||||||
const { _csrf, title, status, entity, ...dynamicFields } = content;
|
|
||||||
const processed: ContentCreateDto & { _csrf?: string } = {
|
|
||||||
_csrf: _csrf?.toString(),
|
|
||||||
entity: entity.toString(),
|
|
||||||
status: !!status,
|
|
||||||
title: title.toString(),
|
|
||||||
dynamicFields,
|
|
||||||
};
|
|
||||||
|
|
||||||
return processed;
|
|
||||||
};
|
|
@ -41,7 +41,6 @@ export const ContentTypes = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// Dialog Controls
|
// Dialog Controls
|
||||||
const addDialogCtl = useDialog<IContentType>(false);
|
const addDialogCtl = useDialog<IContentType>(false);
|
||||||
const editDialogCtl = useDialog<IContentType>(false);
|
|
||||||
const deleteDialogCtl = useDialog<string>(false);
|
const deleteDialogCtl = useDialog<string>(false);
|
||||||
const fieldsDialogCtl = useDialog<IContentType>(false);
|
const fieldsDialogCtl = useDialog<IContentType>(false);
|
||||||
// data fetching
|
// data fetching
|
||||||
@ -119,7 +118,6 @@ export const ContentTypes = () => {
|
|||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Paper>
|
<Paper>
|
||||||
<ContentTypeDialog {...getDisplayDialogs(addDialogCtl)} />
|
<ContentTypeDialog {...getDisplayDialogs(addDialogCtl)} />
|
||||||
<ContentTypeDialog {...getDisplayDialogs(editDialogCtl)} />
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
{...deleteDialogCtl}
|
{...deleteDialogCtl}
|
||||||
callback={() => {
|
callback={() => {
|
||||||
|
@ -119,6 +119,20 @@ const ContentFieldInput: React.FC<ContentFieldInput> = ({
|
|||||||
return <Input {...field} error={!!errors[contentField.name]} />;
|
return <Input {...field} error={!!errors[contentField.name]} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const INITIAL_FIELDS = ["title", "status"];
|
||||||
|
const buildDynamicFields = (
|
||||||
|
content: IContentAttributes,
|
||||||
|
contentType?: IContentType,
|
||||||
|
) => ({
|
||||||
|
title: content.title,
|
||||||
|
entity: content.entity,
|
||||||
|
status: content.status,
|
||||||
|
dynamicFields: {
|
||||||
|
...contentType?.fields
|
||||||
|
?.filter(({ name }) => !INITIAL_FIELDS.includes(name))
|
||||||
|
.reduce((acc, { name }) => ({ ...acc, [name]: content[name] }), {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export type ContentDialogProps = DialogControlProps<{
|
export type ContentDialogProps = DialogControlProps<{
|
||||||
content?: IContent;
|
content?: IContent;
|
||||||
@ -163,7 +177,7 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
|||||||
const onSubmitForm = async (params: IContentAttributes) => {
|
const onSubmitForm = async (params: IContentAttributes) => {
|
||||||
if (content) {
|
if (content) {
|
||||||
updateContent(
|
updateContent(
|
||||||
{ id: content.id, params },
|
{ id: content.id, params: buildDynamicFields(params, contentType) },
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error(t("message.internal_server_error"));
|
toast.error(t("message.internal_server_error"));
|
||||||
@ -176,7 +190,7 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
|||||||
);
|
);
|
||||||
} else if (contentType) {
|
} else if (contentType) {
|
||||||
createContent(
|
createContent(
|
||||||
{ ...params, entity: contentType.id },
|
{ ...buildDynamicFields(params, contentType), entity: contentType.id },
|
||||||
{
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
@ -198,11 +212,7 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (content) {
|
if (content) {
|
||||||
reset({
|
reset(content);
|
||||||
entity: content.entity,
|
|
||||||
status: content.status,
|
|
||||||
title: content.title,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
@ -221,7 +231,11 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name={contentField.name}
|
name={contentField.name}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={content ? content[contentField.name] : null}
|
defaultValue={
|
||||||
|
content
|
||||||
|
? content?.["dynamicFields"]?.[contentField.name]
|
||||||
|
: null
|
||||||
|
}
|
||||||
rules={
|
rules={
|
||||||
contentField.name === "title"
|
contentField.name === "title"
|
||||||
? validationRules.title
|
? validationRules.title
|
||||||
|
@ -165,8 +165,8 @@ export const Contents = () => {
|
|||||||
field: "entity",
|
field: "entity",
|
||||||
headerName: t("label.entity"),
|
headerName: t("label.entity"),
|
||||||
flex: 1,
|
flex: 1,
|
||||||
valueGetter: (row: IContent) => {
|
valueGetter: (entityId) => {
|
||||||
const contentType = getEntityFromCache(row.id);
|
const contentType = getEntityFromCache(entityId);
|
||||||
|
|
||||||
return contentType?.name;
|
return contentType?.name;
|
||||||
},
|
},
|
||||||
|
@ -164,10 +164,14 @@ export const ContentTypeEntity = new schema.Entity(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ContentEntity = new schema.Entity(EntityType.CONTENT, undefined, {
|
export const ContentEntity = new schema.Entity(
|
||||||
idAttribute: ({ id }) => id,
|
EntityType.CONTENT,
|
||||||
processStrategy: processCommonStrategy,
|
{ entity: ContentTypeEntity },
|
||||||
});
|
{
|
||||||
|
idAttribute: ({ id }) => id,
|
||||||
|
processStrategy: processCommonStrategy,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const SettingEntity = new schema.Entity(EntityType.SETTING, {
|
export const SettingEntity = new schema.Entity(EntityType.SETTING, {
|
||||||
idAttribute: ({ id }) => id,
|
idAttribute: ({ id }) => id,
|
||||||
|
Loading…
Reference in New Issue
Block a user