diff --git a/api/src/cms/cms.module.ts b/api/src/cms/cms.module.ts index 660b0430..8c8bf456 100644 --- a/api/src/cms/cms.module.ts +++ b/api/src/cms/cms.module.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 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. @@ -9,11 +9,8 @@ import { forwardRef, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { AttachmentModule } from '@/attachment/attachment.module'; import { ChatModule } from '@/chat/chat.module'; -import { AttachmentModel } from '../attachment/schemas/attachment.schema'; - import { ContentTypeController } from './controllers/content-type.controller'; import { ContentController } from './controllers/content.controller'; import { MenuController } from './controllers/menu.controller'; @@ -29,13 +26,7 @@ import { MenuService } from './services/menu.service'; @Module({ imports: [ - MongooseModule.forFeature([ - ContentModel, - ContentTypeModel, - AttachmentModel, - MenuModel, - ]), - AttachmentModule, + MongooseModule.forFeature([ContentModel, ContentTypeModel, MenuModel]), forwardRef(() => ChatModule), ], controllers: [ContentController, ContentTypeController, MenuController], diff --git a/api/src/cms/controllers/content.controller.spec.ts b/api/src/cms/controllers/content.controller.spec.ts index e18df455..b2fc47a5 100644 --- a/api/src/cms/controllers/content.controller.spec.ts +++ b/api/src/cms/controllers/content.controller.spec.ts @@ -1,24 +1,16 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 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 fs from 'fs'; - import { NotFoundException } from '@nestjs/common/exceptions'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { MongooseModule } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import { AttachmentRepository } from '@/attachment/repositories/attachment.repository'; -import { - Attachment, - AttachmentModel, -} from '@/attachment/schemas/attachment.schema'; -import { AttachmentService } from '@/attachment/services/attachment.service'; import { LoggerService } from '@/logger/logger.service'; import { NOT_FOUND_ID } from '@/utils/constants/mock'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; @@ -48,10 +40,8 @@ describe('ContentController', () => { let contentController: ContentController; let contentService: ContentService; let contentTypeService: ContentTypeService; - let attachmentService: AttachmentService; let contentType: ContentType | null; let content: Content | null; - let attachment: Attachment | null; let updatedContent; let pageQuery: PageQueryDto; @@ -60,34 +50,24 @@ describe('ContentController', () => { controllers: [ContentController], imports: [ rootMongooseTestModule(installContentFixtures), - MongooseModule.forFeature([ - ContentTypeModel, - ContentModel, - AttachmentModel, - ]), + MongooseModule.forFeature([ContentTypeModel, ContentModel]), ], providers: [ LoggerService, ContentTypeService, ContentService, ContentRepository, - AttachmentService, ContentTypeRepository, - AttachmentRepository, EventEmitter2, ], }).compile(); contentController = module.get(ContentController); contentService = module.get(ContentService); - attachmentService = module.get(AttachmentService); contentTypeService = module.get(ContentTypeService); contentType = await contentTypeService.findOne({ name: 'Product' }); content = await contentService.findOne({ title: 'Jean', }); - attachment = await attachmentService.findOne({ - name: 'store1.jpg', - }); pageQuery = getPageQuery({ limit: 1, @@ -243,91 +223,74 @@ describe('ContentController', () => { }); describe('import', () => { + const mockCsvData: string = `other,title,status,image + should not appear,store 3,true,image.jpg`; + + const file: Express.Multer.File = { + buffer: Buffer.from(mockCsvData, 'utf-8'), + originalname: 'test.csv', + mimetype: 'text/csv', + size: mockCsvData.length, + fieldname: 'file', + encoding: '7bit', + stream: null, + destination: '', + filename: '', + path: '', + } as unknown as Express.Multer.File; + it('should import content from a CSV file', async () => { - const mockCsvData: string = `other,title,status,image -should not appear,store 3,true,image.jpg`; - - const mockCsvContentDto: ContentCreateDto = { - entity: '0', - title: 'store 3', - status: true, - dynamicFields: { - image: 'image.jpg', - }, - }; - jest.spyOn(contentService, 'createMany'); - jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true); - jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockCsvData); - - const contentType = await contentTypeService.findOne({ + const mockContentType = { + id: '0', name: 'Store', - }); - - const result = await contentController.import({ - idFileToImport: attachment!.id, - idTargetContentType: contentType!.id, - }); - expect(contentService.createMany).toHaveBeenCalledWith([ - { ...mockCsvContentDto, entity: contentType!.id }, + } as unknown as ContentType; + jest + .spyOn(contentTypeService, 'findOne') + .mockResolvedValueOnce(mockContentType); + jest.spyOn(contentService, 'parseAndSaveDataset').mockResolvedValueOnce([ + { + entity: mockContentType.id, + title: 'store 3', + status: true, + dynamicFields: { + image: 'image.jpg', + }, + id: '', + createdAt: new Date(), + updatedAt: new Date(), + }, ]); - expect(result).toEqualPayload( - [ - { - ...mockCsvContentDto, - entity: contentType!.id, - }, - ], - [...IGNORED_TEST_FIELDS, 'rag'], + const result = await contentController.import(file, mockContentType.id); + expect(contentService.parseAndSaveDataset).toHaveBeenCalledWith( + mockCsvData, + mockContentType.id, + mockContentType, ); + expect(result).toEqualPayload([ + { + entity: mockContentType.id, + title: 'store 3', + status: true, + dynamicFields: { + image: 'image.jpg', + }, + id: '', + }, + ]); }); it('should throw NotFoundException if content type is not found', async () => { + jest.spyOn(contentTypeService, 'findOne').mockResolvedValueOnce(null); await expect( - contentController.import({ - idFileToImport: attachment!.id, - idTargetContentType: NOT_FOUND_ID, - }), + contentController.import(file, 'INVALID_ID'), ).rejects.toThrow(new NotFoundException('Content type is not found')); }); - it('should throw NotFoundException if file is not found in attachment database', async () => { - const contentType = await contentTypeService.findOne({ - name: 'Product', - }); - jest.spyOn(contentTypeService, 'findOne'); - await expect( - contentController.import({ - idFileToImport: NOT_FOUND_ID, - idTargetContentType: contentType!.id.toString(), - }), - ).rejects.toThrow(new NotFoundException('File does not exist')); + it('should throw NotFoundException if idTargetContentType is missing', async () => { + await expect(contentController.import(file, '')).rejects.toThrow( + new NotFoundException('Missing parameter'), + ); }); - - it('should throw NotFoundException if file does not exist in the given path ', async () => { - jest.spyOn(fs, 'existsSync').mockReturnValue(false); - await expect( - contentController.import({ - idFileToImport: attachment!.id, - idTargetContentType: contentType!.id, - }), - ).rejects.toThrow(new NotFoundException('File does not exist')); - }); - - it.each([ - ['file param and content type params are missing', '', ''], - ['content type param is missing', '', NOT_FOUND_ID], - ['file param is missing', NOT_FOUND_ID, ''], - ])( - 'should throw NotFoundException if %s', - async (_message, fileToImport, targetContentType) => { - await expect( - contentController.import({ - idFileToImport: fileToImport, - idTargetContentType: targetContentType, - }), - ).rejects.toThrow(new NotFoundException('Missing params')); - }, - ); }); }); diff --git a/api/src/cms/controllers/content.controller.ts b/api/src/cms/controllers/content.controller.ts index 8913720c..c878caa0 100644 --- a/api/src/cms/controllers/content.controller.ts +++ b/api/src/cms/controllers/content.controller.ts @@ -1,14 +1,11 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 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 fs from 'fs'; -import path from 'path'; - import { Body, Controller, @@ -20,14 +17,12 @@ import { Patch, Post, Query, + UploadedFile, UseInterceptors, } from '@nestjs/common'; -import { BadRequestException } from '@nestjs/common/exceptions'; +import { FileInterceptor } from '@nestjs/platform-express'; import { CsrfCheck } from '@tekuconcept/nestjs-csrf'; -import Papa from 'papaparse'; -import { AttachmentService } from '@/attachment/services/attachment.service'; -import { config } from '@/config'; import { CsrfInterceptor } from '@/interceptors/csrf.interceptor'; import { LoggerService } from '@/logger/logger.service'; import { BaseController } from '@/utils/generics/base-controller'; @@ -59,7 +54,6 @@ export class ContentController extends BaseController< constructor( private readonly contentService: ContentService, private readonly contentTypeService: ContentTypeService, - private readonly attachmentService: AttachmentService, private readonly logger: LoggerService, ) { super(contentService); @@ -92,29 +86,22 @@ export class ContentController extends BaseController< /** * Imports content from a CSV file based on the provided content type and file ID. * - * @param idTargetContentType - The content type to match the CSV data against. - * @param idFileToImport - The ID of the file to be imported. - * + * @param idTargetContentType - The content type to match the CSV data against. * * @returns A promise that resolves to the newly created content documents. */ - @Get('import/:idTargetContentType/:idFileToImport') + @CsrfCheck(true) + @Post('import') + @UseInterceptors(FileInterceptor('file')) async import( - @Param() - { - idTargetContentType: targetContentType, - idFileToImport: fileToImport, - }: { - idTargetContentType: string; - idFileToImport: string; - }, + @UploadedFile() file: Express.Multer.File, + @Query('idTargetContentType') + targetContentType: string, ) { - // Check params - if (!fileToImport || !targetContentType) { - this.logger.warn(`Parameters are missing`); - throw new NotFoundException(`Missing params`); + const datasetContent = file.buffer.toString('utf-8'); + if (!targetContentType) { + this.logger.warn(`Parameter is missing`); + throw new NotFoundException(`Missing parameter`); } - - // Find the content type that corresponds to the given content const contentType = await this.contentTypeService.findOne(targetContentType); if (!contentType) { @@ -124,56 +111,11 @@ export class ContentController extends BaseController< throw new NotFoundException(`Content type is not found`); } - // Get file location - const file = await this.attachmentService.findOne(fileToImport); - // Check if file is present - const filePath = file - ? path.join(config.parameters.uploadDir, file.location) - : undefined; - - if (!file || !filePath || !fs.existsSync(filePath)) { - this.logger.warn(`Failed to find file type with id ${fileToImport}.`); - throw new NotFoundException(`File does not exist`); - } - //read file sync - const data = fs.readFileSync(filePath, 'utf8'); - - const result = Papa.parse>(data, { - header: true, - skipEmptyLines: true, - dynamicTyping: true, - }); - - if (result.errors.length > 0) { - this.logger.warn( - `Errors parsing the file: ${JSON.stringify(result.errors)}`, - ); - - throw new BadRequestException(result.errors, { - cause: result.errors, - description: 'Error while parsing CSV', - }); - } - - const contentsDto = result.data.reduce( - (acc, { title, status, ...rest }) => [ - ...acc, - { - title: String(title), - status: Boolean(status), - entity: targetContentType, - dynamicFields: Object.keys(rest) - .filter((key) => - contentType.fields?.map((field) => field.name).includes(key), - ) - .reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}), - }, - ], - [], + return await this.contentService.parseAndSaveDataset( + datasetContent, + targetContentType, + contentType, ); - - // Create content - return await this.contentService.createMany(contentsDto); } /** diff --git a/api/src/cms/services/content.service.spec.ts b/api/src/cms/services/content.service.spec.ts index 7edd605c..901fb8b7 100644 --- a/api/src/cms/services/content.service.spec.ts +++ b/api/src/cms/services/content.service.spec.ts @@ -10,9 +10,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { MongooseModule } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import { AttachmentRepository } from '@/attachment/repositories/attachment.repository'; -import { AttachmentModel } from '@/attachment/schemas/attachment.schema'; -import { AttachmentService } from '@/attachment/services/attachment.service'; import { OutgoingMessageFormat } from '@/chat/schemas/types/message'; import { ContentOptions } from '@/chat/schemas/types/options'; import { LoggerService } from '@/logger/logger.service'; @@ -44,19 +41,13 @@ describe('ContentService', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ rootMongooseTestModule(installContentFixtures), - MongooseModule.forFeature([ - ContentTypeModel, - ContentModel, - AttachmentModel, - ]), + MongooseModule.forFeature([ContentTypeModel, ContentModel]), ], providers: [ ContentTypeRepository, ContentRepository, - AttachmentRepository, ContentTypeService, ContentService, - AttachmentService, LoggerService, EventEmitter2, ], diff --git a/api/src/cms/services/content.service.ts b/api/src/cms/services/content.service.ts index e4424aea..a865d99b 100644 --- a/api/src/cms/services/content.service.ts +++ b/api/src/cms/services/content.service.ts @@ -6,9 +6,9 @@ * 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 } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import Papa from 'papaparse'; -import { AttachmentService } from '@/attachment/services/attachment.service'; import { StdOutgoingListMessage } from '@/chat/schemas/types/message'; import { ContentOptions } from '@/chat/schemas/types/options'; import { LoggerService } from '@/logger/logger.service'; @@ -17,6 +17,7 @@ import { TFilterQuery } from '@/utils/types/filter.types'; import { ContentDto } from '../dto/content.dto'; import { ContentRepository } from '../repositories/content.repository'; +import { ContentType } from '../schemas/content-type.schema'; import { Content, ContentFull, @@ -32,7 +33,6 @@ export class ContentService extends BaseService< > { constructor( readonly repository: ContentRepository, - private readonly attachmentService: AttachmentService, private readonly logger: LoggerService, ) { super(repository); @@ -103,4 +103,68 @@ export class ContentService extends BaseService< throw err; } } + + /** + * Parses a CSV dataset and saves the content in the repository. + * + * @param data - The CSV data as a string to be parsed. + * @param targetContentType - The content type to associate with the parsed data. + * @param contentType - The content type metadata, including fields to validate the parsed data. + * @return A promise resolving to the created content objects. + */ + async parseAndSaveDataset( + data: string, + targetContentType: string, + contentType: ContentType, + ) { + // Parse local CSV file + const result: { + errors: any[]; + data: Array>; + } = Papa.parse(data, { + header: true, + skipEmptyLines: true, + }); + + if (result.errors && result.errors.length > 0) { + this.logger.warn( + `Errors parsing the file: ${JSON.stringify(result.errors)}`, + ); + throw new BadRequestException(result.errors, { + cause: result.errors, + description: 'Error while parsing CSV', + }); + } + if (!result.data.every((row) => row.title && row.status)) { + throw new BadRequestException( + 'Missing required fields: "title" or "status"', + { + cause: 'Invalid CSV data', + description: 'CSV must include "title" and "status" columns', + }, + ); + } + const contentsDto = result.data.reduce( + (acc, { title, status, ...rest }) => [ + ...acc, + { + title: String(title), + status: Boolean(status), + entity: targetContentType, + dynamicFields: Object.keys(rest) + .filter((key) => + contentType.fields?.map((field) => field.name).includes(key), + ) + .reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}), + }, + ], + [], + ); + this.logger.log(`Parsed ${result.data.length} rows from CSV.`); + try { + return await this.createMany(contentsDto); + } catch (err) { + this.logger.error('Error occurred when extracting data. ', err); + } + } } diff --git a/frontend/src/components/contents/ContentImportForm.tsx b/frontend/src/components/contents/ContentImportForm.tsx deleted file mode 100644 index 9e70ba83..00000000 --- a/frontend/src/components/contents/ContentImportForm.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright © 2025 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 { FC, Fragment, useState } from "react"; -import { useQuery } from "react-query"; - -import AttachmentInput from "@/app-components/attachment/AttachmentInput"; -import { ContentContainer, ContentItem } from "@/app-components/dialogs"; -import { useApiClient } from "@/hooks/useApiClient"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { AttachmentResourceRef } from "@/types/attachment.types"; -import { ComponentFormProps } from "@/types/common/dialogs.types"; -import { IContentType } from "@/types/content-type.types"; - -export type ContentImportFormData = { row: null; contentType: IContentType }; -export const ContentImportForm: FC< - ComponentFormProps -> = ({ data, Wrapper = Fragment, WrapperProps, ...rest }) => { - const [attachmentId, setAttachmentId] = useState(null); - const { t } = useTranslate(); - const { toast } = useToast(); - const { apiClient } = useApiClient(); - const { refetch, isFetching } = useQuery( - ["importContent", data?.contentType.id, attachmentId], - async () => { - if (data?.contentType.id && attachmentId) { - await apiClient.importContent(data.contentType.id, attachmentId); - } - }, - { - enabled: false, - onError: () => { - rest.onError?.(); - toast.error(t("message.internal_server_error")); - }, - onSuccess: () => { - rest.onSuccess?.(); - toast.success(t("message.success_save")); - }, - }, - ); - const handleImportClick = () => { - if (attachmentId && data?.contentType.id) { - refetch(); - } - }; - - return ( - -
- - - { - setAttachmentId(id); - }} - label="" - value={attachmentId} - resourceRef={AttachmentResourceRef.ContentAttachment} - /> - - -
-
- ); -}; diff --git a/frontend/src/components/contents/ContentImportFormDialog.tsx b/frontend/src/components/contents/ContentImportFormDialog.tsx deleted file mode 100644 index 046896ea..00000000 --- a/frontend/src/components/contents/ContentImportFormDialog.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2025 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 { GenericFormDialog } from "@/app-components/dialogs"; -import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; - -import { ContentImportForm, ContentImportFormData } from "./ContentImportForm"; - -export const ContentImportFormDialog = < - T extends ContentImportFormData = ContentImportFormData, ->( - props: ComponentFormDialogProps, -) => ( - - Form={ContentImportForm} - rowKey="row" - addText="button.import" - confirmButtonProps={{ value: "button.import" }} - {...props} - /> -); diff --git a/frontend/src/components/contents/index.tsx b/frontend/src/components/contents/index.tsx index ee639e4b..9cac3d65 100644 --- a/frontend/src/components/contents/index.tsx +++ b/frontend/src/components/contents/index.tsx @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 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. @@ -9,12 +9,13 @@ import { faAlignLeft } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import UploadIcon from "@mui/icons-material/Upload"; -import { Button, Chip, Grid, Paper, Switch, Typography } from "@mui/material"; +import { Button, ButtonGroup, Chip, Grid, Paper, Switch, Typography } from "@mui/material"; import Link from "next/link"; import { useRouter } from "next/router"; +import { useQueryClient } from "react-query"; import { ConfirmDialogBody } from "@/app-components/dialogs"; +import FileUploadButton from "@/app-components/inputs/FileInput"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -22,9 +23,11 @@ import { } from "@/app-components/tables/columns/getColumns"; import { renderHeader } from "@/app-components/tables/columns/renderHeader"; import { DataGrid } from "@/app-components/tables/DataGrid"; +import { isSameEntity } from "@/hooks/crud/helpers"; import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; import { useGet, useGetFromCache } from "@/hooks/crud/useGet"; +import { useImport } from "@/hooks/crud/useImport"; import { useUpdate } from "@/hooks/crud/useUpdate"; import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; @@ -38,12 +41,12 @@ import { PermissionAction } from "@/types/permission.types"; import { getDateTimeFormatter } from "@/utils/date"; import { ContentFormDialog } from "./ContentFormDialog"; -import { ContentImportFormDialog } from "./ContentImportFormDialog"; export const Contents = () => { const { t } = useTranslate(); const { toast } = useToast(); const { query } = useRouter(); + const queryClient = useQueryClient(); const dialogs = useDialogs(); // data fetching const { onSearch, searchPayload } = useSearch({ @@ -51,7 +54,7 @@ export const Contents = () => { $iLike: ["title"], }); const hasPermission = useHasPermission(); - const { dataGridProps, refetch } = useFind( + const { dataGridProps } = useFind( { entity: EntityType.CONTENT, format: Format.FULL }, { params: searchPayload, @@ -97,8 +100,36 @@ export const Contents = () => { const { data: contentType } = useGet(String(query.id), { entity: EntityType.CONTENT_TYPE, }); + const { mutate: importDataset, isLoading } = useImport( + EntityType.CONTENT, + { + onError: () => { + toast.error(t("message.import_failed")); + }, + onSuccess: (data) => { + queryClient.removeQueries({ + predicate: ({ queryKey }) => { + const [_qType, qEntity] = queryKey; - return ( + return ( + isSameEntity(qEntity, EntityType.CONTENT) + ); + }, + }); + if (data.length) { + toast.success(t("message.success_import")); + } else { + toast.error(t("message.import_duplicated_data")); + } + }, + }, + { idTargetContentType: contentType?.id } + ); + const handleImportChange = (file: File) => { + importDataset(file); + }; + +return ( @@ -119,6 +150,7 @@ export const Contents = () => { {hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? ( + - - ) : null} - {hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? ( - - - + > + {t("button.add")} + + + + + + ) : null} diff --git a/frontend/src/hooks/crud/useImport.tsx b/frontend/src/hooks/crud/useImport.tsx index 4444362a..8e4a4e3d 100644 --- a/frontend/src/hooks/crud/useImport.tsx +++ b/frontend/src/hooks/crud/useImport.tsx @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 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. @@ -25,6 +25,7 @@ export const useImport = < TMutationOptions, "mutationFn" | "mutationKey" > = {}, + params: Record = {}, ) => { const api = useEntityApiClient(entity); const queryClient = useQueryClient(); @@ -35,7 +36,7 @@ export const useImport = < return useMutation({ mutationFn: async (variables) => { - const data = await api.import(variables); + const data = await api.import(variables, params); const { result, entities } = normalizeAndCache(data); // Invalidate current entity count and collection diff --git a/frontend/src/services/api.class.ts b/frontend/src/services/api.class.ts index c31e3cb4..9e7b4c06 100644 --- a/frontend/src/services/api.class.ts +++ b/frontend/src/services/api.class.ts @@ -239,14 +239,6 @@ export class ApiClient { return data; } - async importContent(contentTypeId: string, attachmentId: string) { - const { data } = await this.request.get( - `${ROUTES.CONTENT_IMPORT}/${contentTypeId}/${attachmentId}`, - ); - - return data; - } - async predictNlp(text: string) { const { data } = await this.request.get( `${ROUTES.NLP_SAMPLE_PREDICT}`, @@ -288,15 +280,18 @@ export class EntityApiClient extends ApiClient { return data; } - async import(file: File) { + async import(file: File, params?: any) { const { _csrf } = await this.getCsrf(); const formData = new FormData(); formData.append("file", file); const { data } = await this.request.post, FormData>( - `${ROUTES[this.type]}/import?_csrf=${_csrf}`, + `${ROUTES[this.type]}/import`, formData, + { + params: { _csrf , ...params }, + } ); return data;