♻️(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,
|
CalloutBlock,
|
||||||
DividerBlock,
|
DividerBlock,
|
||||||
PdfBlock,
|
PdfBlock,
|
||||||
|
UploadLoaderBlock,
|
||||||
} from './custom-blocks';
|
} from './custom-blocks';
|
||||||
import {
|
import {
|
||||||
InterlinkingLinkInlineContent,
|
InterlinkingLinkInlineContent,
|
||||||
@@ -56,6 +57,7 @@ const baseBlockNoteSchema = withPageBreak(
|
|||||||
divider: DividerBlock,
|
divider: DividerBlock,
|
||||||
image: AccessibleImageBlock,
|
image: AccessibleImageBlock,
|
||||||
pdf: PdfBlock,
|
pdf: PdfBlock,
|
||||||
|
uploadLoader: UploadLoaderBlock,
|
||||||
},
|
},
|
||||||
inlineContentSpecs: {
|
inlineContentSpecs: {
|
||||||
...defaultInlineContentSpecs,
|
...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 './CalloutBlock';
|
||||||
export * from './DividerBlock';
|
export * from './DividerBlock';
|
||||||
export * from './PdfBlock';
|
export * from './PdfBlock';
|
||||||
|
export * from './UploadLoaderBlock';
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { useMediaUrl } from '@/core/config';
|
|||||||
import { sleep } from '@/utils';
|
import { sleep } from '@/utils';
|
||||||
|
|
||||||
import { checkDocMediaStatus, useCreateDocAttachment } from '../api';
|
import { checkDocMediaStatus, useCreateDocAttachment } from '../api';
|
||||||
import Loader from '../assets/loader.svg?url';
|
|
||||||
import Warning from '../assets/warning.svg?url';
|
|
||||||
import { DocsBlockNoteEditor } from '../types';
|
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) => {
|
export const useUploadFile = (docId: string) => {
|
||||||
const {
|
const {
|
||||||
mutateAsync: createDocAttachment,
|
mutateAsync: createDocAttachment,
|
||||||
@@ -122,35 +74,55 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
|||||||
|
|
||||||
// Delay to let the time to the dom to be rendered
|
// Delay to let the time to the dom to be rendered
|
||||||
const timoutId = setTimeout(() => {
|
const timoutId = setTimeout(() => {
|
||||||
replaceUploadContent(
|
// Replace the resource block by a loading block
|
||||||
blockId,
|
const { insertedBlocks, removedBlocks } = editor.replaceBlocks(
|
||||||
informationStatus(Loader.src, t('Analyzing file...')),
|
[blockId],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'uploadLoader',
|
||||||
|
props: {
|
||||||
|
information: t('Analyzing file...'),
|
||||||
|
type: 'loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
loopCheckDocMediaStatus(url)
|
loopCheckDocMediaStatus(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const block = editor.getBlock(blockId);
|
if (insertedBlocks.length === 0 || removedBlocks.length === 0) {
|
||||||
if (!block) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
block.props = {
|
const loadingBlockId = insertedBlocks[0].id;
|
||||||
...block.props,
|
const removedBlock = removedBlocks[0];
|
||||||
|
|
||||||
|
removedBlock.props = {
|
||||||
|
...removedBlock.props,
|
||||||
url: `${mediaUrl}${response.file}`,
|
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) => {
|
.catch((error) => {
|
||||||
console.error('Error analyzing file:', error);
|
console.error('Error analyzing file:', error);
|
||||||
|
|
||||||
replaceUploadContent(
|
const loadingBlock = insertedBlocks[0];
|
||||||
blockId,
|
|
||||||
informationStatus(
|
if (!loadingBlock) {
|
||||||
Warning.src,
|
return;
|
||||||
t('The antivirus has detected an anomaly in your file.'),
|
}
|
||||||
|
|
||||||
|
loadingBlock.props = {
|
||||||
|
...loadingBlock.props,
|
||||||
|
type: 'warning',
|
||||||
|
information: t(
|
||||||
|
'The antivirus has detected an anomaly in your file.',
|
||||||
),
|
),
|
||||||
);
|
};
|
||||||
|
|
||||||
|
editor.updateBlock(loadingBlock.id, loadingBlock);
|
||||||
});
|
});
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ export * from './paragraphPDF';
|
|||||||
export * from './quoteDocx';
|
export * from './quoteDocx';
|
||||||
export * from './quotePDF';
|
export * from './quotePDF';
|
||||||
export * from './tablePDF';
|
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,
|
blockMappingDividerDocx,
|
||||||
blockMappingImageDocx,
|
blockMappingImageDocx,
|
||||||
blockMappingQuoteDocx,
|
blockMappingQuoteDocx,
|
||||||
|
blockMappingUploadLoaderDocx,
|
||||||
} from './blocks-mapping';
|
} from './blocks-mapping';
|
||||||
import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping';
|
import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping';
|
||||||
import { DocsExporterDocx } from './types';
|
import { DocsExporterDocx } from './types';
|
||||||
@@ -22,6 +23,7 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
|||||||
pdf: docxDefaultSchemaMappings.blockMapping.file as any,
|
pdf: docxDefaultSchemaMappings.blockMapping.file as any,
|
||||||
quote: blockMappingQuoteDocx,
|
quote: blockMappingQuoteDocx,
|
||||||
image: blockMappingImageDocx,
|
image: blockMappingImageDocx,
|
||||||
|
uploadLoader: blockMappingUploadLoaderDocx,
|
||||||
},
|
},
|
||||||
inlineContentMapping: {
|
inlineContentMapping: {
|
||||||
...docxDefaultSchemaMappings.inlineContentMapping,
|
...docxDefaultSchemaMappings.inlineContentMapping,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
blockMappingParagraphPDF,
|
blockMappingParagraphPDF,
|
||||||
blockMappingQuotePDF,
|
blockMappingQuotePDF,
|
||||||
blockMappingTablePDF,
|
blockMappingTablePDF,
|
||||||
|
blockMappingUploadLoaderPDF,
|
||||||
} from './blocks-mapping';
|
} from './blocks-mapping';
|
||||||
import { inlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping';
|
import { inlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping';
|
||||||
import { DocsExporterPDF } from './types';
|
import { DocsExporterPDF } from './types';
|
||||||
@@ -27,6 +28,7 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
|
|||||||
// The types don't match exactly but the implementation is compatible
|
// The types don't match exactly but the implementation is compatible
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
pdf: pdfDefaultSchemaMappings.blockMapping.file as any,
|
pdf: pdfDefaultSchemaMappings.blockMapping.file as any,
|
||||||
|
uploadLoader: blockMappingUploadLoaderPDF,
|
||||||
},
|
},
|
||||||
inlineContentMapping: {
|
inlineContentMapping: {
|
||||||
...pdfDefaultSchemaMappings.inlineContentMapping,
|
...pdfDefaultSchemaMappings.inlineContentMapping,
|
||||||
|
|||||||
Reference in New Issue
Block a user