mirror of
https://github.com/hexastack/hexabot
synced 2025-05-07 22:34:46 +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).
|
||||
*/
|
||||
|
||||
import {
|
||||
forwardRef,
|
||||
MiddlewareConsumer,
|
||||
Module,
|
||||
NestModule,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
|
||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||
@ -21,7 +15,6 @@ import { ChatModule } from '@/chat/chat.module';
|
||||
import { ContentTypeController } from './controllers/content-type.controller';
|
||||
import { ContentController } from './controllers/content.controller';
|
||||
import { MenuController } from './controllers/menu.controller';
|
||||
import { ContentMiddleWare } from './middlewares/content.middleware';
|
||||
import { ContentTypeRepository } from './repositories/content-type.repository';
|
||||
import { ContentRepository } from './repositories/content.repository';
|
||||
import { MenuRepository } from './repositories/menu.repository';
|
||||
@ -55,13 +48,4 @@ import { AttachmentModel } from '../attachment/schemas/attachment.schema';
|
||||
],
|
||||
exports: [MenuService, ContentService, ContentTypeService],
|
||||
})
|
||||
export class CmsModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(ContentMiddleWare)
|
||||
.forRoutes(
|
||||
{ path: 'content', method: RequestMethod.POST },
|
||||
{ path: 'content/:id', method: RequestMethod.PATCH },
|
||||
);
|
||||
}
|
||||
}
|
||||
export class CmsModule {}
|
||||
|
@ -35,7 +35,6 @@ import {
|
||||
|
||||
import { ContentController } from './content.controller';
|
||||
import { ContentCreateDto } from '../dto/content.dto';
|
||||
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
|
||||
import { ContentTypeRepository } from '../repositories/content-type.repository';
|
||||
import { ContentRepository } from '../repositories/content.repository';
|
||||
import { ContentType, ContentTypeModel } from '../schemas/content-type.schema';
|
||||
@ -48,7 +47,6 @@ describe('ContentController', () => {
|
||||
let contentService: ContentService;
|
||||
let contentTypeService: ContentTypeService;
|
||||
let attachmentService: AttachmentService;
|
||||
let transformInterceptor: ContentTransformInterceptor;
|
||||
let contentType: ContentType;
|
||||
let content: Content;
|
||||
let attachment: Attachment;
|
||||
@ -74,7 +72,6 @@ describe('ContentController', () => {
|
||||
AttachmentService,
|
||||
ContentTypeRepository,
|
||||
AttachmentRepository,
|
||||
ContentTransformInterceptor,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
@ -82,9 +79,6 @@ describe('ContentController', () => {
|
||||
contentService = module.get<ContentService>(ContentService);
|
||||
attachmentService = module.get<AttachmentService>(AttachmentService);
|
||||
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
|
||||
transformInterceptor = module.get<ContentTransformInterceptor>(
|
||||
ContentTransformInterceptor,
|
||||
);
|
||||
contentType = await contentTypeService.findOne({ name: 'Product' });
|
||||
content = await contentService.findOne({
|
||||
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', () => {
|
||||
it('should return the number of contents', async () => {
|
||||
jest.spyOn(contentService, 'count');
|
||||
|
@ -39,8 +39,7 @@ import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { ContentTypeService } from './../services/content-type.service';
|
||||
import { ContentService } from './../services/content.service';
|
||||
import { ContentCreateDto } from '../dto/content.dto';
|
||||
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
|
||||
import { ContentCreateDto, ContentUpdateDto } from '../dto/content.dto';
|
||||
import { ContentType } from '../schemas/content-type.schema';
|
||||
import {
|
||||
Content,
|
||||
@ -48,9 +47,8 @@ import {
|
||||
ContentPopulate,
|
||||
ContentStub,
|
||||
} from '../schemas/content.schema';
|
||||
import { preprocessDynamicFields } from '../utilities';
|
||||
|
||||
@UseInterceptors(ContentTransformInterceptor, CsrfInterceptor)
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('content')
|
||||
export class ContentController extends BaseController<
|
||||
Content,
|
||||
@ -116,8 +114,7 @@ export class ContentController extends BaseController<
|
||||
entity: contentType?.id,
|
||||
},
|
||||
});
|
||||
const newContent = this.filterDynamicFields(contentDto, contentType);
|
||||
return await this.contentService.create(newContent);
|
||||
return await this.contentService.create(contentDto);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,12 +183,22 @@ export class ContentController extends BaseController<
|
||||
});
|
||||
}
|
||||
|
||||
const contentsDto = result.data.map((content) => {
|
||||
content.entity = targetContentType;
|
||||
const dto = preprocessDynamicFields(content);
|
||||
// Match headers against entity fields
|
||||
return this.filterDynamicFields(dto, contentType);
|
||||
});
|
||||
const contentsDto = result.data.reduce(
|
||||
(acc, { title, status, ...rest }) => [
|
||||
...acc,
|
||||
{
|
||||
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
|
||||
return await this.contentService.createMany(contentsDto);
|
||||
@ -319,16 +326,12 @@ export class ContentController extends BaseController<
|
||||
* @returns The updated content document.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch('/:id')
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Body() contentDto: ContentCreateDto,
|
||||
@Body() contentDto: ContentUpdateDto,
|
||||
@Param('id') id: string,
|
||||
): Promise<Content> {
|
||||
const contentType = await this.contentTypeService.findOne(
|
||||
contentDto.entity,
|
||||
);
|
||||
const newContent = this.filterDynamicFields(contentDto, contentType);
|
||||
const updatedContent = await this.contentService.updateOne(id, newContent);
|
||||
const updatedContent = await this.contentService.updateOne(id, contentDto);
|
||||
if (!updatedContent) {
|
||||
this.logger.warn(
|
||||
`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).
|
||||
*/
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';
|
||||
import { IsString, IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
|
||||
|
||||
import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
||||
@ -32,3 +32,5 @@ export class ContentCreateDto {
|
||||
@IsOptional()
|
||||
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();
|
||||
// Dialog Controls
|
||||
const addDialogCtl = useDialog<IContentType>(false);
|
||||
const editDialogCtl = useDialog<IContentType>(false);
|
||||
const deleteDialogCtl = useDialog<string>(false);
|
||||
const fieldsDialogCtl = useDialog<IContentType>(false);
|
||||
// data fetching
|
||||
@ -119,7 +118,6 @@ export const ContentTypes = () => {
|
||||
<Grid item xs={12}>
|
||||
<Paper>
|
||||
<ContentTypeDialog {...getDisplayDialogs(addDialogCtl)} />
|
||||
<ContentTypeDialog {...getDisplayDialogs(editDialogCtl)} />
|
||||
<DeleteDialog
|
||||
{...deleteDialogCtl}
|
||||
callback={() => {
|
||||
|
@ -119,6 +119,20 @@ const ContentFieldInput: React.FC<ContentFieldInput> = ({
|
||||
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<{
|
||||
content?: IContent;
|
||||
@ -163,7 +177,7 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
||||
const onSubmitForm = async (params: IContentAttributes) => {
|
||||
if (content) {
|
||||
updateContent(
|
||||
{ id: content.id, params },
|
||||
{ id: content.id, params: buildDynamicFields(params, contentType) },
|
||||
{
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
@ -176,7 +190,7 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
||||
);
|
||||
} else if (contentType) {
|
||||
createContent(
|
||||
{ ...params, entity: contentType.id },
|
||||
{ ...buildDynamicFields(params, contentType), entity: contentType.id },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error);
|
||||
@ -198,11 +212,7 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (content) {
|
||||
reset({
|
||||
entity: content.entity,
|
||||
status: content.status,
|
||||
title: content.title,
|
||||
});
|
||||
reset(content);
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
@ -221,7 +231,11 @@ export const ContentDialog: FC<ContentDialogProps> = ({
|
||||
<Controller
|
||||
name={contentField.name}
|
||||
control={control}
|
||||
defaultValue={content ? content[contentField.name] : null}
|
||||
defaultValue={
|
||||
content
|
||||
? content?.["dynamicFields"]?.[contentField.name]
|
||||
: null
|
||||
}
|
||||
rules={
|
||||
contentField.name === "title"
|
||||
? validationRules.title
|
||||
|
@ -165,8 +165,8 @@ export const Contents = () => {
|
||||
field: "entity",
|
||||
headerName: t("label.entity"),
|
||||
flex: 1,
|
||||
valueGetter: (row: IContent) => {
|
||||
const contentType = getEntityFromCache(row.id);
|
||||
valueGetter: (entityId) => {
|
||||
const contentType = getEntityFromCache(entityId);
|
||||
|
||||
return contentType?.name;
|
||||
},
|
||||
|
@ -164,10 +164,14 @@ export const ContentTypeEntity = new schema.Entity(
|
||||
},
|
||||
);
|
||||
|
||||
export const ContentEntity = new schema.Entity(EntityType.CONTENT, undefined, {
|
||||
idAttribute: ({ id }) => id,
|
||||
processStrategy: processCommonStrategy,
|
||||
});
|
||||
export const ContentEntity = new schema.Entity(
|
||||
EntityType.CONTENT,
|
||||
{ entity: ContentTypeEntity },
|
||||
{
|
||||
idAttribute: ({ id }) => id,
|
||||
processStrategy: processCommonStrategy,
|
||||
},
|
||||
);
|
||||
|
||||
export const SettingEntity = new schema.Entity(EntityType.SETTING, {
|
||||
idAttribute: ({ id }) => id,
|
||||
|
Loading…
Reference in New Issue
Block a user