Skip to content

Commit cc4bed6

Browse files
committed
♻️(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.
1 parent d8f90c0 commit cc4bed6

File tree

9 files changed

+104
-62
lines changed

9 files changed

+104
-62
lines changed

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
CalloutBlock,
3939
DividerBlock,
4040
PdfBlock,
41+
UploadLoaderBlock,
4142
} from './custom-blocks';
4243
import {
4344
InterlinkingLinkInlineContent,
@@ -56,6 +57,7 @@ const baseBlockNoteSchema = withPageBreak(
5657
divider: DividerBlock,
5758
image: AccessibleImageBlock,
5859
pdf: PdfBlock,
60+
uploadLoader: UploadLoaderBlock,
5961
},
6062
inlineContentSpecs: {
6163
...defaultInlineContentSpecs,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createReactBlockSpec } from '@blocknote/react';
2+
3+
import { Box, Text } from '@/components';
4+
5+
import Loader from '../../assets/loader.svg';
6+
import Warning from '../../assets/warning.svg';
7+
8+
export const UploadLoaderBlock = createReactBlockSpec(
9+
{
10+
type: 'uploadLoader',
11+
propSchema: {
12+
information: { default: '' as const },
13+
type: {
14+
default: 'loading' as const,
15+
values: ['loading', 'warning'] as const,
16+
},
17+
},
18+
content: 'none',
19+
},
20+
{
21+
render: ({ block }) => {
22+
return (
23+
<Box className="bn-visual-media-wrapper" $direction="row" $gap="0.5rem">
24+
{block.props.type === 'warning' ? (
25+
<Warning />
26+
) : (
27+
<Loader style={{ animation: 'spin 1.5s linear infinite' }} />
28+
)}
29+
<Text>{block.props.information}</Text>
30+
</Box>
31+
);
32+
},
33+
},
34+
);

src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './AccessibleImageBlock';
22
export * from './CalloutBlock';
33
export * from './DividerBlock';
44
export * from './PdfBlock';
5+
export * from './UploadLoaderBlock';

src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx

Lines changed: 34 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { useMediaUrl } from '@/core/config';
66
import { sleep } from '@/utils';
77

88
import { checkDocMediaStatus, useCreateDocAttachment } from '../api';
9-
import Loader from '../assets/loader.svg?url';
10-
import Warning from '../assets/warning.svg?url';
119
import { DocsBlockNoteEditor } from '../types';
1210

1311
/**
@@ -33,52 +31,6 @@ const loopCheckDocMediaStatus = async (url: string) => {
3331
}
3432
};
3533

36-
const informationStatus = (src: string, text: string) => {
37-
const loadingContainer = document.createElement('div');
38-
loadingContainer.style.display = 'flex';
39-
loadingContainer.style.alignItems = 'center';
40-
loadingContainer.style.justifyContent = 'left';
41-
loadingContainer.style.padding = '10px';
42-
loadingContainer.style.color = '#666';
43-
loadingContainer.className =
44-
'bn-visual-media bn-audio bn-file-name-with-icon';
45-
46-
// Create an image element for the SVG
47-
const imgElement = document.createElement('img');
48-
imgElement.src = src;
49-
50-
// Create a text span
51-
const textSpan = document.createElement('span');
52-
textSpan.textContent = text;
53-
textSpan.style.marginLeft = '8px';
54-
textSpan.style.verticalAlign = 'middle';
55-
imgElement.style.animation = 'spin 1.5s linear infinite';
56-
57-
// Add the spinner and text to the container
58-
loadingContainer.appendChild(imgElement);
59-
loadingContainer.appendChild(textSpan);
60-
61-
return loadingContainer;
62-
};
63-
64-
const replaceUploadContent = (blockId: string, elementReplace: HTMLElement) => {
65-
const blockEl = document.body.querySelector(
66-
`.bn-block[data-id="${blockId}"]`,
67-
);
68-
69-
blockEl
70-
?.querySelector('.bn-visual-media-wrapper .bn-visual-media')
71-
?.replaceWith(elementReplace);
72-
73-
blockEl
74-
?.querySelector('.bn-file-block-content-wrapper .bn-audio')
75-
?.replaceWith(elementReplace);
76-
77-
blockEl
78-
?.querySelector('.bn-file-block-content-wrapper .bn-file-name-with-icon')
79-
?.replaceWith(elementReplace);
80-
};
81-
8234
export const useUploadFile = (docId: string) => {
8335
const {
8436
mutateAsync: createDocAttachment,
@@ -122,35 +74,55 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
12274

12375
// Delay to let the time to the dom to be rendered
12476
const timoutId = setTimeout(() => {
125-
replaceUploadContent(
126-
blockId,
127-
informationStatus(Loader.src, t('Analyzing file...')),
77+
// Replace the resource block by a loading block
78+
const { insertedBlocks, removedBlocks } = editor.replaceBlocks(
79+
[blockId],
80+
[
81+
{
82+
type: 'uploadLoader',
83+
props: {
84+
information: t('Analyzing file...'),
85+
type: 'loading',
86+
},
87+
},
88+
],
12889
);
12990

13091
loopCheckDocMediaStatus(url)
13192
.then((response) => {
132-
const block = editor.getBlock(blockId);
133-
if (!block) {
93+
if (insertedBlocks.length === 0 || removedBlocks.length === 0) {
13494
return;
13595
}
13696

137-
block.props = {
138-
...block.props,
97+
const loadingBlockId = insertedBlocks[0].id;
98+
const removedBlock = removedBlocks[0];
99+
100+
removedBlock.props = {
101+
...removedBlock.props,
139102
url: `${mediaUrl}${response.file}`,
140103
};
141104

142-
editor.updateBlock(blockId, block);
105+
// Replace the loading block with the resource block (image, audio, video, pdf ...)
106+
editor.replaceBlocks([loadingBlockId], [removedBlock]);
143107
})
144108
.catch((error) => {
145109
console.error('Error analyzing file:', error);
146110

147-
replaceUploadContent(
148-
blockId,
149-
informationStatus(
150-
Warning.src,
151-
t('The antivirus has detected an anomaly in your file.'),
111+
const loadingBlock = insertedBlocks[0];
112+
113+
if (!loadingBlock) {
114+
return;
115+
}
116+
117+
loadingBlock.props = {
118+
...loadingBlock.props,
119+
type: 'warning',
120+
information: t(
121+
'The antivirus has detected an anomaly in your file.',
152122
),
153-
);
123+
};
124+
125+
editor.updateBlock(loadingBlock.id, loadingBlock);
154126
});
155127
}, 250);
156128

src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ export * from './paragraphPDF';
99
export * from './quoteDocx';
1010
export * from './quotePDF';
1111
export * from './tablePDF';
12+
export * from './uploadLoaderPDF';
13+
export * from './uploadLoaderDocx';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Paragraph, TextRun } from 'docx';
2+
3+
import { DocsExporterDocx } from '../types';
4+
5+
export const blockMappingUploadLoaderDocx: DocsExporterDocx['mappings']['blockMapping']['uploadLoader'] =
6+
(block) => {
7+
return new Paragraph({
8+
children: [
9+
new TextRun(block.props.type === 'loading' ? '⏳' : '⚠️'),
10+
new TextRun(' '),
11+
new TextRun(block.props.information),
12+
],
13+
});
14+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Text, View } from '@react-pdf/renderer';
2+
3+
import { DocsExporterPDF } from '../types';
4+
5+
export const blockMappingUploadLoaderPDF: DocsExporterPDF['mappings']['blockMapping']['uploadLoader'] =
6+
(block) => {
7+
return (
8+
<View wrap={false} style={{ flexDirection: 'row', gap: 4 }}>
9+
<Text>{block.props.type === 'loading' ? '⏳' : '⚠️'}</Text>
10+
<Text>{block.props.information}</Text>
11+
</View>
12+
);
13+
};

src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
blockMappingDividerDocx,
77
blockMappingImageDocx,
88
blockMappingQuoteDocx,
9+
blockMappingUploadLoaderDocx,
910
} from './blocks-mapping';
1011
import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping';
1112
import { DocsExporterDocx } from './types';
@@ -22,6 +23,7 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
2223
pdf: docxDefaultSchemaMappings.blockMapping.file as any,
2324
quote: blockMappingQuoteDocx,
2425
image: blockMappingImageDocx,
26+
uploadLoader: blockMappingUploadLoaderDocx,
2527
},
2628
inlineContentMapping: {
2729
...docxDefaultSchemaMappings.inlineContentMapping,

src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
blockMappingParagraphPDF,
99
blockMappingQuotePDF,
1010
blockMappingTablePDF,
11+
blockMappingUploadLoaderPDF,
1112
} from './blocks-mapping';
1213
import { inlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping';
1314
import { DocsExporterPDF } from './types';
@@ -27,6 +28,7 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
2728
// The types don't match exactly but the implementation is compatible
2829
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2930
pdf: pdfDefaultSchemaMappings.blockMapping.file as any,
31+
uploadLoader: blockMappingUploadLoaderPDF,
3032
},
3133
inlineContentMapping: {
3234
...pdfDefaultSchemaMappings.inlineContentMapping,

0 commit comments

Comments
 (0)