From 510199cf040ea6b987061b47a37d4d617873640b Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:23:56 +0100 Subject: [PATCH] feat(ee): docx word export (#2294) * vendorize prosemirror-docx - wip * feat(ee): docx word export * sync --- .../src/components/common/export-modal.tsx | 90 +- apps/client/src/ee/features.ts | 1 + .../features/page/services/page-service.ts | 19 + .../src/features/page/types/page.types.ts | 1 + apps/client/src/lib/api-client.ts | 6 +- apps/client/tsconfig.json | 4 +- apps/server/package.json | 41 +- apps/server/src/common/features.ts | 1 + apps/server/src/ee | 2 +- package.json | 43 +- packages/editor-ext/package.json | 1 + packages/editor-ext/src/index.ts | 4 + .../src/lib/prosemirror-docx/README.md | 167 ++++ .../src/lib/prosemirror-docx/index.ts | 24 + .../src/lib/prosemirror-docx/numbering.ts | 47 + .../src/lib/prosemirror-docx/schema.ts | 250 +++++ .../src/lib/prosemirror-docx/serializer.ts | 925 ++++++++++++++++++ .../src/lib/prosemirror-docx/types.ts | 34 + .../src/lib/prosemirror-docx/utils.ts | 91 ++ pnpm-lock.yaml | 123 ++- 20 files changed, 1771 insertions(+), 103 deletions(-) create mode 100644 packages/editor-ext/src/lib/prosemirror-docx/README.md create mode 100644 packages/editor-ext/src/lib/prosemirror-docx/index.ts create mode 100644 packages/editor-ext/src/lib/prosemirror-docx/numbering.ts create mode 100644 packages/editor-ext/src/lib/prosemirror-docx/schema.ts create mode 100644 packages/editor-ext/src/lib/prosemirror-docx/serializer.ts create mode 100644 packages/editor-ext/src/lib/prosemirror-docx/types.ts create mode 100644 packages/editor-ext/src/lib/prosemirror-docx/utils.ts diff --git a/apps/client/src/components/common/export-modal.tsx b/apps/client/src/components/common/export-modal.tsx index 2a83debf9..bbd58b64d 100644 --- a/apps/client/src/components/common/export-modal.tsx +++ b/apps/client/src/components/common/export-modal.tsx @@ -6,13 +6,21 @@ import { Select, Switch, Divider, + Tooltip, + Badge, } from "@mantine/core"; -import { exportPage } from "@/features/page/services/page-service.ts"; +import { + exportPage, + exportPageToDocx, +} from "@/features/page/services/page-service.ts"; import { useState } from "react"; import { ExportFormat } from "@/features/page/types/page.types.ts"; import { notifications } from "@mantine/notifications"; import { exportSpace } from "@/features/space/services/space-service"; import { useTranslation } from "react-i18next"; +import { Feature } from "@/ee/features"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; interface ExportModalProps { id: string; @@ -32,17 +40,25 @@ export default function ExportModal({ const [includeAttachments, setIncludeAttachments] = useState(false); const [isExporting, setIsExporting] = useState(false); const { t } = useTranslation(); + const upgradeLabel = useUpgradeLabel(); + const isDocx = format === ExportFormat.Docx; + const docxEntitled = useHasFeature(Feature.DOCX_EXPORT); + const blockedByLicense = isDocx && !docxEntitled; const handleExport = async () => { setIsExporting(true); try { if (type === "page") { - await exportPage({ - pageId: id, - format, - includeChildren, - includeAttachments, - }); + if (format === ExportFormat.Docx) { + await exportPageToDocx({ pageId: id }); + } else { + await exportPage({ + pageId: id, + format, + includeChildren, + includeAttachments, + }); + } } if (type === "space") { await exportSpace({ spaceId: id, format, includeAttachments }); @@ -88,10 +104,15 @@ export default function ExportModal({
{t("Format")}
- + - {type === "page" && ( + {type === "page" && !isDocx && ( <> @@ -143,7 +164,16 @@ export default function ExportModal({ - + + + @@ -154,23 +184,49 @@ export default function ExportModal({ interface ExportFormatSelection { format: ExportFormat; onChange: (value: string) => void; + includeDocx?: boolean; + docxEntitled?: boolean; } -function ExportFormatSelection({ format, onChange }: ExportFormatSelection) { +function ExportFormatSelection({ + format, + onChange, + includeDocx, + docxEntitled, +}: ExportFormatSelection) { const { t } = useTranslation(); + const data = [ + { value: "markdown", label: "Markdown" }, + { value: "html", label: "HTML" }, + ...(includeDocx + ? [{ value: "docx", label: "Word (.docx)", disabled: !docxEntitled }] + : []), + ]; + return (