From 944ea131c49d8f75716eebb5d05f0b48cd8959ab Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 31 Jul 2026 15:37:32 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=84(frontend)=20allowed=20partially=20?= =?UTF-8?q?export=20when=20MIT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When MIT, we were not able to export anything. But actually, the export HTML and Print are allowed for MIT, so we should allow them. We now allow partially export when MIT, but the AGPL export is still not allowed (pdf / odt / docx). --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-export.spec.ts | 2 +- .../doc-export/__tests__/ExportMIT.test.tsx | 10 +- .../doc-export/components/ModalExport.tsx | 145 ++++++------------ .../features/docs/doc-export/hooks/index.ts | 18 +++ .../docs/doc-export/hooks/useExportAGPL.tsx | 98 ++++++++++++ .../src/features/docs/doc-export/index.ts | 12 +- .../__tests__/DocToolBoxLicence.spec.tsx | 67 -------- .../__tests__/DocToolBoxLicenceMIT.spec.tsx | 67 -------- .../doc-management/components/DocToolBox.tsx | 20 +-- 10 files changed, 176 insertions(+), 264 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/hooks/index.ts create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/hooks/useExportAGPL.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-management/__tests__/DocToolBoxLicence.spec.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-management/__tests__/DocToolBoxLicenceMIT.spec.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e075b1af..e4e42d1b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to ### Fixed - 🐛(frontend) fix clipped formatting toolbar in new comment composer #2585 +- 📄(frontend) allowed partially export when MIT #2551 ## [v5.5.0] - 2026-08-24 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts index dec8aa1b1..c6dfbb683 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts @@ -30,7 +30,7 @@ test.describe('Doc Export', () => { await expect(page.getByTestId('modal-export-title')).toBeVisible(); await expect( page.getByText( - 'Export your document to download in .docx, .odt, .pdf or .html(zip) format.', + 'Export your document to download in .pdf, .docx, .odt or .html(zip) format.', ), ).toBeVisible(); await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx index 3e5f3c3c0..b614a74cd 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx @@ -1,7 +1,7 @@ import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; -vi.mock('@/docs/doc-export/components/ModalExport', () => ({ - ModalExport: vi.fn(), +vi.mock('@/docs/doc-export/hooks/useExportAGPL', () => ({ + useExportAGPL: vi.fn(), })); const originalEnv = process.env.NEXT_PUBLIC_PUBLISH_AS_MIT; @@ -18,15 +18,15 @@ describe('useModuleExport', () => { it('should return undefined when NEXT_PUBLIC_PUBLISH_AS_MIT is true', async () => { process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'true'; - const Export = await import('@/features/docs/doc-export/'); + const Export = await import('@/docs/doc-export/hooks'); expect(Export.default).toBeUndefined(); }); it('should load modules when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => { process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false'; - const Export = await import('@/features/docs/doc-export/'); + const Export = await import('@/docs/doc-export/hooks'); - expect(Export.default).toHaveProperty('ModalExport'); + expect(Export.default).toHaveProperty('useExportAGPL'); }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx index 8333d191c..70a518898 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx @@ -1,6 +1,3 @@ -import { DOCXExporter } from '@blocknote/xl-docx-exporter'; -import { ODTExporter } from '@blocknote/xl-odt-exporter'; -import { PDFExporter } from '@blocknote/xl-pdf-exporter'; import { Button, Loader, @@ -10,30 +7,19 @@ import { VariantType, useToastProvider, } from '@gouvfr-lasuite/cunningham-react'; -import { DocumentProps, pdf } from '@react-pdf/renderer'; -import jsonemoji from 'emoji-datasource-apple' with { type: 'json' }; import i18next from 'i18next'; import JSZip from 'jszip'; -import { - cloneElement, - isValidElement, - useEffect, - useRef, - useState, -} from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Box, ButtonCloseModal, Text } from '@/components'; import { useMediaUrl } from '@/core'; import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore'; -import { Doc, useTrans } from '@/docs/doc-management'; +import { type Doc, useTrans } from '@/docs/doc-management'; import { fallbackLng } from '@/i18n/config'; -import { exportCorsResolveFileUrl } from '../api/exportResolveFileUrl'; -import { docxDocsSchemaMappings } from '../mappingDocx'; -import { odtDocsSchemaMappings } from '../mappingODT'; -import { pdfDocsSchemaMappings } from '../mappingPDF'; +import ModulesExport from '../hooks/'; import { downloadFile } from '../utils'; import { addMediaFilesToZip, @@ -41,12 +27,7 @@ import { improveHtmlAccessibility, } from '../utils_html'; -enum DocDownloadFormat { - HTML = 'html', - PDF = 'pdf', - DOCX = 'docx', - ODT = 'odt', -} +const useExportAGPL = ModulesExport?.useExportAGPL; interface ModalExportProps { onClose: () => void; @@ -58,12 +39,13 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => { const { toast } = useToastProvider(); const { editor } = useEditorStore(); const [isExporting, setIsExporting] = useState(false); - const [format, setFormat] = useState( - DocDownloadFormat.PDF, - ); const { untitledDocument } = useTrans(); const mediaUrl = useMediaUrl(); const selectRef = useRef(null); + const exportAGPL = useExportAGPL?.(doc, editor); + const [format, setFormat] = useState( + exportAGPL?.formats.find((opt) => opt.value === 'pdf')?.value || 'html', + ); useEffect(() => { const frameId = requestAnimationFrame(() => { @@ -75,16 +57,31 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => { return () => cancelAnimationFrame(frameId); }, []); - const formatOptions = [ - { label: t('PDF'), value: DocDownloadFormat.PDF }, - { label: t('Docx'), value: DocDownloadFormat.DOCX }, - { label: t('ODT'), value: DocDownloadFormat.ODT }, - { label: t('HTML'), value: DocDownloadFormat.HTML }, - ]; + const formatSelect = useMemo(() => { + const formatOptions = (exportAGPL?.formats || []).concat([ + { + label: t('HTML'), + value: 'html', + labelDescription: t('.html(zip)'), + }, + ]); - const formatLabels = Object.fromEntries( - formatOptions.map((opt) => [opt.value, opt.label]), - ); + const formatLabels = Object.fromEntries( + formatOptions.map((opt) => [opt.value, opt.label]), + ); + + const labels = formatOptions.map((opt) => opt.labelDescription); + const or = t('or', { + description: + 'Word joining the last two items of the list of available export formats', + }); + const allFormatsLabel = + labels.length > 1 + ? `${labels.slice(0, -1).join(', ')} ${or} ${labels[labels.length - 1]}` + : labels.join(''); + + return { formatOptions, formatLabels, allFormatsLabel }; + }, [t, exportAGPL?.formats]); async function onSubmit() { if (!editor) { @@ -102,62 +99,9 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => { const documentTitle = doc.title || untitledDocument; - const exportDocument = editor.document; - let blobExport: Blob; - if (format === DocDownloadFormat.PDF) { - const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, { - resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url), - emojiSource: { - format: 'png', - builder(code) { - const emojisFound = jsonemoji.filter( - (e) => - e.unified.split('-')[0].toLowerCase() === - code.split('-')[0].toLowerCase(), - ); + let blobExport = await exportAGPL?.docToBlob(format, documentTitle); - const emoji = emojisFound.find((e) => - e.unified.toLocaleLowerCase().includes(code.toLowerCase()), - ); - - if (emoji) { - return `/assets/fonts/emoji/${emoji.image}`; - } - - return '/assets/fonts/emoji/fallback.png'; - }, - }, - }); - const rawPdfDocument = (await exporter.toReactPDFDocument( - exportDocument, - )) as React.ReactElement; - - // Add language, title and outline properties to improve PDF accessibility and navigation - const pdfDocument = isValidElement(rawPdfDocument) - ? cloneElement(rawPdfDocument, { - language: i18next.language, - title: documentTitle, - pageMode: 'useOutlines', - }) - : rawPdfDocument; - - blobExport = await pdf(pdfDocument).toBlob(); - } else if (format === DocDownloadFormat.DOCX) { - const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, { - resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url), - }); - - blobExport = await exporter.toBlob(exportDocument, { - documentOptions: { title: documentTitle }, - sectionOptions: {}, - }); - } else if (format === DocDownloadFormat.ODT) { - const exporter = new ODTExporter(editor.schema, odtDocsSchemaMappings, { - resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url), - }); - - blobExport = await exporter.toODTDocument(exportDocument); - } else if (format === DocDownloadFormat.HTML) { + if (!blobExport && format === 'html') { // Use BlockNote "full HTML" export so that we stay closer to the editor rendering. const fullHtml = await editor.blocksToFullHTML(); @@ -190,14 +134,15 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => { zip.file('styles.css', cssContent); blobExport = await zip.generateAsync({ type: 'blob' }); - } else { + } + + if (!blobExport) { toast(t('The export failed'), VariantType.ERROR); setIsExporting(false); return; } - const downloadExtension = - format === DocDownloadFormat.HTML ? 'zip' : format; + const downloadExtension = format === 'html' ? 'zip' : format; downloadFile(blobExport, `${filename}.${downloadExtension}`); @@ -235,7 +180,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {