♻️(frontend) add upload loader block

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.
This commit is contained in:
Anthony LC
2025-09-26 17:12:32 +02:00
parent d8f90c04bd
commit cc4bed6f8e
9 changed files with 104 additions and 62 deletions

View File

@@ -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,

View File

@@ -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 (
<Box className="bn-visual-media-wrapper" $direction="row" $gap="0.5rem">
{block.props.type === 'warning' ? (
<Warning />
) : (
<Loader style={{ animation: 'spin 1.5s linear infinite' }} />
)}
<Text>{block.props.information}</Text>
</Box>
);
},
},
);

View File

@@ -2,3 +2,4 @@ export * from './AccessibleImageBlock';
export * from './CalloutBlock';
export * from './DividerBlock';
export * from './PdfBlock';
export * from './UploadLoaderBlock';

View File

@@ -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);

View File

@@ -9,3 +9,5 @@ export * from './paragraphPDF';
export * from './quoteDocx';
export * from './quotePDF';
export * from './tablePDF';
export * from './uploadLoaderPDF';
export * from './uploadLoaderDocx';

View File

@@ -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),
],
});
};

View File

@@ -0,0 +1,13 @@
import { Text, View } from '@react-pdf/renderer';
import { DocsExporterPDF } from '../types';
export const blockMappingUploadLoaderPDF: DocsExporterPDF['mappings']['blockMapping']['uploadLoader'] =
(block) => {
return (
<View wrap={false} style={{ flexDirection: 'row', gap: 4 }}>
<Text>{block.props.type === 'loading' ? '⏳' : '⚠️'}</Text>
<Text>{block.props.information}</Text>
</View>
);
};

View File

@@ -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,

View File

@@ -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,