From cc4bed6f8e92afebf271bbc11108f8471a3f5cce Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 26 Sep 2025 17:12:32 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20add=20upload=20l?= =?UTF-8?q?oader=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The way we were handling the antivirus upload loader was not optimal, it didn't work well with the pdf embed block. We created a dedicated upload loader block, it will replace the previous implementation, it is more Blocknote idiomatic and will work better with any type of upload files. --- .../doc-editor/components/BlockNoteEditor.tsx | 2 + .../custom-blocks/UploadLoaderBlock.tsx | 34 +++++++ .../components/custom-blocks/index.ts | 1 + .../docs/doc-editor/hook/useUploadFile.tsx | 96 +++++++------------ .../docs/doc-export/blocks-mapping/index.ts | 2 + .../blocks-mapping/uploadLoaderDocx.tsx | 14 +++ .../blocks-mapping/uploadLoaderPDF.tsx | 13 +++ .../features/docs/doc-export/mappingDocx.tsx | 2 + .../features/docs/doc-export/mappingPDF.tsx | 2 + 9 files changed, 104 insertions(+), 62 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderDocx.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/uploadLoaderPDF.tsx 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 3eb1a71e..68d7c269 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 @@ -38,6 +38,7 @@ import { CalloutBlock, DividerBlock, PdfBlock, + UploadLoaderBlock, } from './custom-blocks'; import { InterlinkingLinkInlineContent, @@ -56,6 +57,7 @@ const baseBlockNoteSchema = withPageBreak( divider: DividerBlock, image: AccessibleImageBlock, pdf: PdfBlock, + uploadLoader: UploadLoaderBlock, }, inlineContentSpecs: { ...defaultInlineContentSpecs, 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 00000000..297b70bf --- /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 1a2ea21e..7aad893b 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 @@ -2,3 +2,4 @@ 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 9ba2b483..d8a0878f 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 e3e766db..e8d7328a 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 00000000..57b26eb5 --- /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 00000000..48e83814 --- /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 73e66d11..0a26644a 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'; @@ -22,6 +23,7 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = { 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 1b72ab1c..ed100e84 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'; @@ -27,6 +28,7 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = { // 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,