♻️(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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -2,3 +2,4 @@ export * from './AccessibleImageBlock';
|
||||
export * from './CalloutBlock';
|
||||
export * from './DividerBlock';
|
||||
export * from './PdfBlock';
|
||||
export * from './UploadLoaderBlock';
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -9,3 +9,5 @@ export * from './paragraphPDF';
|
||||
export * from './quoteDocx';
|
||||
export * from './quotePDF';
|
||||
export * from './tablePDF';
|
||||
export * from './uploadLoaderPDF';
|
||||
export * from './uploadLoaderDocx';
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user