diff --git a/.github/workflows/impress.yml b/.github/workflows/impress.yml index 8cc2ff824e..2e01cd6804 100644 --- a/.github/workflows/impress.yml +++ b/.github/workflows/impress.yml @@ -79,6 +79,7 @@ jobs: --check-filenames \ --ignore-words-list "Dokument,afterAll,excpt,statics" \ --skip "./git/" \ + --skip "**/*.pdf" \ --skip "**/*.po" \ --skip "**/*.pot" \ --skip "**/*.json" \ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6badccff55..a35e49d91c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to ## [Unreleased] +### Added + +- ✨(frontend) add pdf block to the editor #1293 + ### Changed - ♻️(frontend) replace Arial font-family with token font #1411 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/assets/test-pdf.pdf b/src/frontend/apps/e2e/__tests__/app-impress/assets/test-pdf.pdf new file mode 100644 index 0000000000..f0ea5df4e3 Binary files /dev/null and b/src/frontend/apps/e2e/__tests__/app-impress/assets/test-pdf.pdf differ diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index 36520cb77f..fcb4374d5c 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -840,4 +840,38 @@ test.describe('Doc Editor', () => { ).toBeInViewport(); await expect(editor.getByText('Hello Child 14')).not.toBeInViewport(); }); + + test('it embeds PDF', async ({ page, browserName }) => { + await createDoc(page, 'doc-toolbar', browserName, 1); + + await openSuggestionMenu({ page }); + await page.getByText('Embed a PDF file').click(); + + const pdfBlock = page.locator('div[data-content-type="pdf"]').first(); + + await expect(pdfBlock).toBeVisible(); + + await page.getByText('Add PDF').click(); + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.getByText('Upload file').click(); + const fileChooser = await fileChooserPromise; + + console.log(path.join(__dirname, 'assets/test-pdf.pdf')); + await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf')); + + // Wait for the media-check to be processed + await page.waitForTimeout(1000); + + const pdfEmbed = page + .locator('.--docs--editor-container embed.bn-visual-media') + .first(); + + // Check src of pdf + expect(await pdfEmbed.getAttribute('src')).toMatch( + /http:\/\/localhost:8083\/media\/.*\/attachments\/.*.pdf/, + ); + + await expect(pdfEmbed).toHaveAttribute('type', 'application/pdf'); + await expect(pdfEmbed).toHaveAttribute('role', 'presentation'); + }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index b12e5981cf..68d7c269c5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -37,6 +37,8 @@ import { AccessibleImageBlock, CalloutBlock, DividerBlock, + PdfBlock, + UploadLoaderBlock, } from './custom-blocks'; import { InterlinkingLinkInlineContent, @@ -54,6 +56,8 @@ const baseBlockNoteSchema = withPageBreak( callout: CalloutBlock, divider: DividerBlock, image: AccessibleImageBlock, + pdf: PdfBlock, + uploadLoader: UploadLoaderBlock, }, inlineContentSpecs: { ...defaultInlineContentSpecs, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx index 4e8c6e3091..62999dc885 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx @@ -18,6 +18,7 @@ import { import { getCalloutReactSlashMenuItems, getDividerReactSlashMenuItems, + getPdfReactSlashMenuItems, } from './custom-blocks'; import { useGetInterlinkingMenuItems } from './custom-inline-content'; import XLMultiColumn from './xl-multi-column'; @@ -32,7 +33,10 @@ export const BlockNoteSuggestionMenu = () => { DocsStyleSchema >(); const { t } = useTranslation(); - const basicBlocksName = useDictionary().slash_menu.page_break.group; + const dictionaryDate = useDictionary(); + const basicBlocksName = dictionaryDate.slash_menu.page_break.group; + const fileBlocksName = dictionaryDate.slash_menu.file.group; + const getInterlinkingMenuItems = useGetInterlinkingMenuItems(); const getSlashMenuItems = useMemo(() => { @@ -56,11 +60,12 @@ export const BlockNoteSuggestionMenu = () => { getMultiColumnSlashMenuItems?.(editor) || [], getPageBreakReactSlashMenuItems(editor), getDividerReactSlashMenuItems(editor, t, basicBlocksName), + getPdfReactSlashMenuItems(editor, t, fileBlocksName), ), query, ), ); - }, [basicBlocksName, editor, getInterlinkingMenuItems, t]); + }, [basicBlocksName, editor, getInterlinkingMenuItems, t, fileBlocksName]); return ( [0]['editor']; + +export const PdfBlock = createReactBlockSpec( + { + type: 'pdf', + content: 'none', + propSchema: { + name: { default: '' as const }, + url: { default: '' as const }, + caption: { default: '' as const }, + showPreview: { default: true }, + previewWidth: { default: undefined, type: 'number' }, + }, + isFileBlock: true, + fileBlockAccept: ['application/pdf'], + }, + { + render: ({ editor, block, contentRef }) => { + const { t } = useTranslation(); + const pdfUrl = block.props.url; + + return ( + + + } + block={block} + editor={editor as unknown as FileBlockEditor} + buttonText={t('Add PDF')} + > + editor.setTextCursorPosition(block)} + /> + + + ); + }, + }, +); + +export const getPdfReactSlashMenuItems = ( + editor: DocsBlockNoteEditor, + t: TFunction<'translation', undefined>, + group: string, +) => [ + { + title: t('PDF'), + onItemClick: () => { + insertOrUpdateBlock(editor, { type: 'pdf' }); + }, + aliases: [t('pdf'), t('document'), t('embed'), t('file')], + group, + icon: , + subtext: t('Embed a PDF file'), + }, +]; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx new file mode 100644 index 0000000000..297b70bf82 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx @@ -0,0 +1,34 @@ +import { createReactBlockSpec } from '@blocknote/react'; + +import { Box, Text } from '@/components'; + +import Loader from '../../assets/loader.svg'; +import Warning from '../../assets/warning.svg'; + +export const UploadLoaderBlock = createReactBlockSpec( + { + type: 'uploadLoader', + propSchema: { + information: { default: '' as const }, + type: { + default: 'loading' as const, + values: ['loading', 'warning'] as const, + }, + }, + content: 'none', + }, + { + render: ({ block }) => { + return ( + + {block.props.type === 'warning' ? ( + + ) : ( + + )} + {block.props.information} + + ); + }, + }, +); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts index 99c1ee271e..7aad893b84 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts @@ -1,3 +1,5 @@ export * from './AccessibleImageBlock'; export * from './CalloutBlock'; export * from './DividerBlock'; +export * from './PdfBlock'; +export * from './UploadLoaderBlock'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx index 9ba2b483ee..d8a0878fe9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx @@ -6,8 +6,6 @@ import { useMediaUrl } from '@/core/config'; import { sleep } from '@/utils'; import { checkDocMediaStatus, useCreateDocAttachment } from '../api'; -import Loader from '../assets/loader.svg?url'; -import Warning from '../assets/warning.svg?url'; import { DocsBlockNoteEditor } from '../types'; /** @@ -33,52 +31,6 @@ const loopCheckDocMediaStatus = async (url: string) => { } }; -const informationStatus = (src: string, text: string) => { - const loadingContainer = document.createElement('div'); - loadingContainer.style.display = 'flex'; - loadingContainer.style.alignItems = 'center'; - loadingContainer.style.justifyContent = 'left'; - loadingContainer.style.padding = '10px'; - loadingContainer.style.color = '#666'; - loadingContainer.className = - 'bn-visual-media bn-audio bn-file-name-with-icon'; - - // Create an image element for the SVG - const imgElement = document.createElement('img'); - imgElement.src = src; - - // Create a text span - const textSpan = document.createElement('span'); - textSpan.textContent = text; - textSpan.style.marginLeft = '8px'; - textSpan.style.verticalAlign = 'middle'; - imgElement.style.animation = 'spin 1.5s linear infinite'; - - // Add the spinner and text to the container - loadingContainer.appendChild(imgElement); - loadingContainer.appendChild(textSpan); - - return loadingContainer; -}; - -const replaceUploadContent = (blockId: string, elementReplace: HTMLElement) => { - const blockEl = document.body.querySelector( - `.bn-block[data-id="${blockId}"]`, - ); - - blockEl - ?.querySelector('.bn-visual-media-wrapper .bn-visual-media') - ?.replaceWith(elementReplace); - - blockEl - ?.querySelector('.bn-file-block-content-wrapper .bn-audio') - ?.replaceWith(elementReplace); - - blockEl - ?.querySelector('.bn-file-block-content-wrapper .bn-file-name-with-icon') - ?.replaceWith(elementReplace); -}; - export const useUploadFile = (docId: string) => { const { mutateAsync: createDocAttachment, @@ -122,35 +74,55 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => { // Delay to let the time to the dom to be rendered const timoutId = setTimeout(() => { - replaceUploadContent( - blockId, - informationStatus(Loader.src, t('Analyzing file...')), + // Replace the resource block by a loading block + const { insertedBlocks, removedBlocks } = editor.replaceBlocks( + [blockId], + [ + { + type: 'uploadLoader', + props: { + information: t('Analyzing file...'), + type: 'loading', + }, + }, + ], ); loopCheckDocMediaStatus(url) .then((response) => { - const block = editor.getBlock(blockId); - if (!block) { + if (insertedBlocks.length === 0 || removedBlocks.length === 0) { return; } - block.props = { - ...block.props, + const loadingBlockId = insertedBlocks[0].id; + const removedBlock = removedBlocks[0]; + + removedBlock.props = { + ...removedBlock.props, url: `${mediaUrl}${response.file}`, }; - editor.updateBlock(blockId, block); + // Replace the loading block with the resource block (image, audio, video, pdf ...) + editor.replaceBlocks([loadingBlockId], [removedBlock]); }) .catch((error) => { console.error('Error analyzing file:', error); - replaceUploadContent( - blockId, - informationStatus( - Warning.src, - t('The antivirus has detected an anomaly in your file.'), + const loadingBlock = insertedBlocks[0]; + + if (!loadingBlock) { + return; + } + + loadingBlock.props = { + ...loadingBlock.props, + type: 'warning', + information: t( + 'The antivirus has detected an anomaly in your file.', ), - ); + }; + + editor.updateBlock(loadingBlock.id, loadingBlock); }); }, 250); diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts index e3e766dbcb..e8d7328a7a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts @@ -9,3 +9,5 @@ export * from './paragraphPDF'; export * from './quoteDocx'; export * from './quotePDF'; export * from './tablePDF'; +export * from './uploadLoaderPDF'; +export * from './uploadLoaderDocx'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderDocx.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderDocx.tsx new file mode 100644 index 0000000000..57b26eb5a6 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderDocx.tsx @@ -0,0 +1,14 @@ +import { Paragraph, TextRun } from 'docx'; + +import { DocsExporterDocx } from '../types'; + +export const blockMappingUploadLoaderDocx: DocsExporterDocx['mappings']['blockMapping']['uploadLoader'] = + (block) => { + return new Paragraph({ + children: [ + new TextRun(block.props.type === 'loading' ? '⏳' : '⚠️'), + new TextRun(' '), + new TextRun(block.props.information), + ], + }); + }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderPDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderPDF.tsx new file mode 100644 index 0000000000..48e838147e --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderPDF.tsx @@ -0,0 +1,13 @@ +import { Text, View } from '@react-pdf/renderer'; + +import { DocsExporterPDF } from '../types'; + +export const blockMappingUploadLoaderPDF: DocsExporterPDF['mappings']['blockMapping']['uploadLoader'] = + (block) => { + return ( + + {block.props.type === 'loading' ? '⏳' : '⚠️'} + {block.props.information} + + ); + }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx index 5c44f06073..0a26644acc 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx @@ -6,6 +6,7 @@ import { blockMappingDividerDocx, blockMappingImageDocx, blockMappingQuoteDocx, + blockMappingUploadLoaderDocx, } from './blocks-mapping'; import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping'; import { DocsExporterDocx } from './types'; @@ -16,8 +17,13 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = { ...docxDefaultSchemaMappings.blockMapping, callout: blockMappingCalloutDocx, divider: blockMappingDividerDocx, + // We're using the file block mapping for PDF blocks + // The types don't match exactly but the implementation is compatible + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pdf: docxDefaultSchemaMappings.blockMapping.file as any, quote: blockMappingQuoteDocx, image: blockMappingImageDocx, + uploadLoader: blockMappingUploadLoaderDocx, }, inlineContentMapping: { ...docxDefaultSchemaMappings.inlineContentMapping, diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx index 9e96b32a4c..ed100e8452 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx @@ -8,6 +8,7 @@ import { blockMappingParagraphPDF, blockMappingQuotePDF, blockMappingTablePDF, + blockMappingUploadLoaderPDF, } from './blocks-mapping'; import { inlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping'; import { DocsExporterPDF } from './types'; @@ -23,6 +24,11 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = { divider: blockMappingDividerPDF, quote: blockMappingQuotePDF, table: blockMappingTablePDF, + // We're using the file block mapping for PDF blocks + // The types don't match exactly but the implementation is compatible + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pdf: pdfDefaultSchemaMappings.blockMapping.file as any, + uploadLoader: blockMappingUploadLoaderPDF, }, inlineContentMapping: { ...pdfDefaultSchemaMappings.inlineContentMapping,