diff --git a/apps/client/src/features/page/components/page-import-modal.tsx b/apps/client/src/features/page/components/page-import-modal.tsx index be0264b6..c805edba 100644 --- a/apps/client/src/features/page/components/page-import-modal.tsx +++ b/apps/client/src/features/page/components/page-import-modal.tsx @@ -11,6 +11,7 @@ import { IconBrandNotion, IconCheck, IconFileCode, + IconFileTypeDocx, IconFileTypeZip, IconMarkdown, IconX, @@ -86,11 +87,13 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { const markdownFileRef = useRef<() => void>(null); const htmlFileRef = useRef<() => void>(null); + const docxFileRef = useRef<() => void>(null); const notionFileRef = useRef<() => void>(null); const confluenceFileRef = useRef<() => void>(null); const zipFileRef = useRef<() => void>(null); const canUseConfluence = isCloud() || workspace?.hasLicenseKey; + const canUseDocx = isCloud() || workspace?.hasLicenseKey; const handleZipUpload = async (selectedFile: File, source: string) => { if (!selectedFile) { @@ -265,6 +268,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { // Reset file inputs after successful upload if (markdownFileRef.current) markdownFileRef.current(); if (htmlFileRef.current) htmlFileRef.current(); + if (docxFileRef.current) docxFileRef.current(); const pageCountText = pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`; @@ -321,6 +325,30 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { )} + + {(props) => ( + + + + )} + + handleZipUpload(file, "notion")} accept="application/zip" diff --git a/apps/server/src/ee b/apps/server/src/ee index 6d3eb76d..d93f53e3 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 6d3eb76d4ef04ad84fb9a5e724de6f94343921cc +Subproject commit d93f53e3c792bd644637a3ede2f72b2f3565a493 diff --git a/apps/server/src/integrations/import/import.controller.ts b/apps/server/src/integrations/import/import.controller.ts index 1adb82eb..11842a51 100644 --- a/apps/server/src/integrations/import/import.controller.ts +++ b/apps/server/src/integrations/import/import.controller.ts @@ -44,7 +44,7 @@ export class ImportController { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { - const validFileExtensions = ['.md', '.html']; + const validFileExtensions = ['.md', '.html', '.docx']; const maxFileSize = bytes('10mb'); diff --git a/apps/server/src/integrations/import/services/import.service.ts b/apps/server/src/integrations/import/services/import.service.ts index aeeebcee..a6aec5c5 100644 --- a/apps/server/src/integrations/import/services/import.service.ts +++ b/apps/server/src/integrations/import/services/import.service.ts @@ -29,6 +29,7 @@ import { StorageService } from '../../storage/storage.service'; import { InjectQueue } from '@nestjs/bullmq'; import { Queue } from 'bullmq'; import { QueueJob, QueueName } from '../../queue/constants'; +import { ModuleRef } from '@nestjs/core'; @Injectable() export class ImportService { @@ -40,6 +41,7 @@ export class ImportService { @InjectKysely() private readonly db: KyselyDB, @InjectQueue(QueueName.FILE_TASK_QUEUE) private readonly fileTaskQueue: Queue, + private moduleRef: ModuleRef, ) {} async importPage( @@ -59,11 +61,22 @@ export class ImportService { let prosemirrorState = null; let createdPage = null; + // For DOCX, we need the page ID upfront so images can reference it + const pageId = fileExtension === '.docx' ? uuid7() : undefined; + try { if (fileExtension.endsWith('.md')) { prosemirrorState = await this.processMarkdown(fileContent); } else if (fileExtension.endsWith('.html')) { prosemirrorState = await this.processHTML(fileContent); + } else if (fileExtension.endsWith('.docx')) { + prosemirrorState = await this.processDocx( + fileBuffer, + workspaceId, + spaceId, + pageId, + userId, + ); } } catch (err) { const message = 'Error processing file content'; @@ -87,6 +100,7 @@ export class ImportService { const pagePosition = await this.getNewPagePosition(spaceId); createdPage = await this.pageRepo.insertPage({ + ...(pageId ? { id: pageId } : {}), slugId: generateSlugId(), title: pageTitle, content: prosemirrorJson, @@ -129,6 +143,42 @@ export class ImportService { } } + async processDocx( + fileBuffer: Buffer, + workspaceId: string, + spaceId: string, + pageId: string, + userId: string, + ): Promise { + let DocxImportModule: any; + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + DocxImportModule = require('./../../../ee/docx-import/docx-import.service'); + } catch (err) { + this.logger.error( + 'DOCX import requested but EE module not bundled in this build', + ); + throw new BadRequestException( + 'This feature requires a valid enterprise license.', + ); + } + + const docxImportService = this.moduleRef.get( + DocxImportModule.DocxImportService, + { strict: false }, + ); + + const html = await docxImportService.convertDocxToHtml( + fileBuffer, + workspaceId, + spaceId, + pageId, + userId, + ); + + return this.processHTML(html); + } + async createYdoc(prosemirrorJson: any): Promise { if (prosemirrorJson) { // this.logger.debug(`Converting prosemirror json state to ydoc`);