diff --git a/apps/client/src/ee/features.ts b/apps/client/src/ee/features.ts index a9ab8b0d..cacf851f 100644 --- a/apps/client/src/ee/features.ts +++ b/apps/client/src/ee/features.ts @@ -8,6 +8,7 @@ export const Feature = { AI: 'ai', CONFLUENCE_IMPORT: 'import:confluence', DOCX_IMPORT: 'import:docx', + PDF_IMPORT: 'import:pdf', ATTACHMENT_INDEXING: 'attachment:indexing', SECURITY_SETTINGS: 'security:settings', MCP: 'mcp', 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 df6691d5..087beeac 100644 --- a/apps/client/src/features/page/components/page-import-modal.tsx +++ b/apps/client/src/features/page/components/page-import-modal.tsx @@ -12,6 +12,7 @@ import { IconCheck, IconFileCode, IconFileTypeDocx, + IconFileTypePdf, IconFileTypeZip, IconMarkdown, IconX, @@ -90,12 +91,14 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { const markdownFileRef = useRef<() => void>(null); const htmlFileRef = useRef<() => void>(null); const docxFileRef = useRef<() => void>(null); + const pdfFileRef = useRef<() => void>(null); const notionFileRef = useRef<() => void>(null); const confluenceFileRef = useRef<() => void>(null); const zipFileRef = useRef<() => void>(null); const canUseConfluence = useHasFeature(Feature.CONFLUENCE_IMPORT); const canUseDocx = useHasFeature(Feature.DOCX_IMPORT); + const canUsePdf = useHasFeature(Feature.PDF_IMPORT); const upgradeLabel = useUpgradeLabel(); const handleZipUpload = async (selectedFile: File, source: string) => { @@ -298,6 +301,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { if (markdownFileRef.current) markdownFileRef.current(); if (htmlFileRef.current) htmlFileRef.current(); if (docxFileRef.current) docxFileRef.current(); + if (pdfFileRef.current) pdfFileRef.current(); const pageCountText = pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`; @@ -378,6 +382,30 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { )} + + {(props) => ( + + + + )} + + handleZipUpload(file, "notion")} accept="application/zip" diff --git a/apps/server/src/common/features.ts b/apps/server/src/common/features.ts index 38f226a8..c5fd9a20 100644 --- a/apps/server/src/common/features.ts +++ b/apps/server/src/common/features.ts @@ -8,6 +8,7 @@ export const Feature = { AI: 'ai', CONFLUENCE_IMPORT: 'import:confluence', DOCX_IMPORT: 'import:docx', + PDF_IMPORT: 'import:pdf', ATTACHMENT_INDEXING: 'attachment:indexing', SECURITY_SETTINGS: 'security:settings', MCP: 'mcp', diff --git a/apps/server/src/ee b/apps/server/src/ee index a5eb8d1e..4bac9b0a 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit a5eb8d1e9a1bdb914321041929f88dd4296542bb +Subproject commit 4bac9b0a3fe6c238351b0814d49cce7f5fa2e4af diff --git a/apps/server/src/integrations/import/import.controller.ts b/apps/server/src/integrations/import/import.controller.ts index 7ee325e5..69cb4937 100644 --- a/apps/server/src/integrations/import/import.controller.ts +++ b/apps/server/src/integrations/import/import.controller.ts @@ -51,7 +51,7 @@ export class ImportController { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { - const validFileExtensions = ['.md', '.html', '.docx']; + const validFileExtensions = ['.md', '.html', '.docx', '.pdf']; const maxFileSize = bytes('20mb'); @@ -102,6 +102,7 @@ export class ImportController { '.md': 'markdown', '.html': 'html', '.docx': 'docx', + '.pdf': 'pdf', }; if (createdPage) { diff --git a/apps/server/src/integrations/import/services/import.service.ts b/apps/server/src/integrations/import/services/import.service.ts index 231a6c89..78cf0f23 100644 --- a/apps/server/src/integrations/import/services/import.service.ts +++ b/apps/server/src/integrations/import/services/import.service.ts @@ -62,7 +62,10 @@ export class ImportService { let createdPage = null; // For DOCX, we need the page ID upfront so images can reference it - const pageId = fileExtension === '.docx' ? uuid7() : undefined; + const pageId = + fileExtension === '.docx' || fileExtension === '.pdf' + ? uuid7() + : undefined; try { if (fileExtension.endsWith('.md')) { @@ -77,6 +80,14 @@ export class ImportService { pageId, userId, ); + } else if (fileExtension.endsWith('.pdf')) { + prosemirrorState = await this.processPdf( + fileBuffer, + workspaceId, + spaceId, + pageId, + userId, + ); } } catch (err) { const message = 'Error processing file content'; @@ -153,7 +164,7 @@ export class ImportService { let DocxImportModule: any; try { // eslint-disable-next-line @typescript-eslint/no-require-imports - DocxImportModule = require('./../../../ee/docx-import/docx-import.service'); + DocxImportModule = require('./../../../ee/document-import/docx-import.service'); } catch (err) { this.logger.error( 'DOCX import requested but EE module not bundled in this build', @@ -179,6 +190,42 @@ export class ImportService { return this.processHTML(html); } + async processPdf( + fileBuffer: Buffer, + workspaceId: string, + spaceId: string, + pageId: string, + userId: string, + ): Promise { + let PdfImportModule: any; + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + PdfImportModule = require('./../../../ee/document-import/pdf-import.service'); + } catch (err) { + this.logger.error( + 'PDF import requested but EE module not bundled in this build', + ); + throw new BadRequestException( + 'This feature requires a valid enterprise license.', + ); + } + + const pdfImportService = this.moduleRef.get( + PdfImportModule.PdfImportService, + { strict: false }, + ); + + const html = await pdfImportService.convertPdfToHtml( + 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`);