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) => (
+
+ }
+ {...props}
+ >
+ Word (DOCX)
+
+
+ )}
+
+
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`);