From 91217b3c4fcf3d29e719a1706f2f761321876b6a Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 4 Nov 2025 09:32:04 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20retry=20check=20media?= =?UTF-8?q?=20status=20after=20page=20reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous refactoring removed the retry logic for checking media status after a page reload. This commit reintroduces that functionality to ensure uploads are properly processed even after a page reload. We improve the test coverage to validate this behavior. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-editor.spec.ts | 8 +- .../doc-editor/api/checkDocMediaStatus.tsx | 26 +++ .../components/custom-blocks/PdfBlock.tsx | 2 +- .../custom-blocks/UploadLoaderBlock.tsx | 134 +++++++++++++-- .../docs/doc-editor/hook/useUploadFile.tsx | 153 ++++-------------- 6 files changed, 189 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a86e0f2..a2cd9de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to - ♿(frontend) improve accessibility: - ♿(frontend) remove empty alt on logo due to Axe a11y error #1516 - 🐛(backend) fix s3 version_id validation +- 🐛(frontend) retry check media status after page reload #1555 ## [3.8.2] - 2025-10-17 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index ccc35814..d784c3a8 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -494,7 +494,7 @@ test.describe('Doc Editor', () => { if (request.method().includes('GET')) { await route.fulfill({ json: { - status: requestCount ? 'ready' : 'processing', + status: requestCount > 1 ? 'ready' : 'processing', file: '/anything.html', }, }); @@ -518,6 +518,12 @@ test.describe('Doc Editor', () => { await fileChooser.setFiles(path.join(__dirname, 'assets/test.html')); await expect(editor.getByText('Analyzing file...')).toBeVisible(); + + // To be sure the retry happens even after a page reload + await page.reload(); + + await expect(editor.getByText('Analyzing file...')).toBeVisible(); + // The retry takes a few seconds await expect(editor.getByText('test.html')).toBeVisible({ timeout: 7000, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/api/checkDocMediaStatus.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/api/checkDocMediaStatus.tsx index 81e3825e..d0841a05 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/api/checkDocMediaStatus.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/api/checkDocMediaStatus.tsx @@ -1,4 +1,5 @@ import { APIError, errorCauses } from '@/api'; +import { sleep } from '@/utils'; interface CheckDocMediaStatusResponse { file?: string; @@ -25,3 +26,28 @@ export const checkDocMediaStatus = async ({ return response.json() as Promise; }; + +/** + * Upload file can be analyzed on the server side, + * we had this function to wait for the analysis to be done + * before returning the file url. It will keep the loader + * on the upload button until the analysis is done. + * @param url + * @returns Promise status_code + * @description Waits for the upload to be analyzed by checking the status of the file. + */ +export const loopCheckDocMediaStatus = async ( + url: string, +): Promise => { + const SLEEP_TIME = 5000; + const response = await checkDocMediaStatus({ + urlMedia: url, + }); + + if (response.status === 'ready') { + return response; + } else { + await sleep(SLEEP_TIME); + return await loopCheckDocMediaStatus(url); + } +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx index b79421a8..7b8bafeb 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx @@ -46,7 +46,7 @@ type CreatePDFBlockConfig = BlockConfig< interface PdfBlockComponentProps { block: BlockNoDefaults< - Record<'callout', CreatePDFBlockConfig>, + Record<'pdf', CreatePDFBlockConfig>, InlineContentSchema, StyleSchema >; 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 index 297b70bf..3842cb42 100644 --- 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 @@ -1,34 +1,138 @@ +import { + BlockConfig, + BlockNoDefaults, + BlockNoteEditor, + InlineContentSchema, + StyleSchema, +} from '@blocknote/core'; import { createReactBlockSpec } from '@blocknote/react'; +import { t } from 'i18next'; +import { useEffect } from 'react'; import { Box, Text } from '@/components'; +import { useMediaUrl } from '@/core'; +import { loopCheckDocMediaStatus } from '../../api'; import Loader from '../../assets/loader.svg'; import Warning from '../../assets/warning.svg'; +type UploadLoaderPropSchema = { + readonly information: { readonly default: '' }; + readonly type: { + readonly default: 'loading'; + readonly values: readonly ['loading', 'warning']; + }; + readonly blockUploadName: { readonly default: '' }; + readonly blockUploadShowPreview: { readonly default: true }; + readonly blockUploadType: { + readonly default: ''; + }; + readonly blockUploadUrl: { readonly default: '' }; +}; + +type UploadLoaderBlockConfig = BlockConfig< + 'uploadLoader', + UploadLoaderPropSchema, + 'none' +>; + +type UploadLoaderBlockType = BlockNoDefaults< + Record<'uploadLoader', UploadLoaderBlockConfig>, + InlineContentSchema, + StyleSchema +>; + +type UploadLoaderEditor = BlockNoteEditor< + Record<'uploadLoader', UploadLoaderBlockConfig>, + InlineContentSchema, + StyleSchema +>; + +interface UploadLoaderBlockComponentProps { + block: UploadLoaderBlockType; + editor: UploadLoaderEditor; + contentRef: (node: HTMLElement | null) => void; +} + +const UploadLoaderBlockComponent = ({ + block, + editor, +}: UploadLoaderBlockComponentProps) => { + const mediaUrl = useMediaUrl(); + + useEffect(() => { + if (!block.props.blockUploadUrl || block.props.type !== 'loading') { + return; + } + + const url = block.props.blockUploadUrl; + + loopCheckDocMediaStatus(url) + .then((response) => { + // Replace the loading block with the resource block (image, audio, video, pdf ...) + editor.replaceBlocks( + [block.id], + [ + { + type: block.props.blockUploadType, + props: { + url: `${mediaUrl}${response.file}`, + showPreview: block.props.blockUploadShowPreview, + name: block.props.blockUploadName, + caption: '', + backgroundColor: 'default', + textAlignment: 'left', + }, + } as never, + ], + ); + }) + .catch((error) => { + console.error('Error analyzing file:', error); + + editor.updateBlock(block.id, { + type: 'uploadLoader', + props: { + type: 'warning', + information: t( + 'The antivirus has detected an anomaly in your file.', + ), + }, + }); + }); + }, [block, editor, mediaUrl]); + + return ( + + {block.props.type === 'warning' ? ( + + ) : ( + + )} + {block.props.information} + + ); +}; + export const UploadLoaderBlock = createReactBlockSpec( { type: 'uploadLoader', propSchema: { - information: { default: '' as const }, + information: { default: '' }, type: { - default: 'loading' as const, - values: ['loading', 'warning'] as const, + default: 'loading', + values: ['loading', 'warning'], }, + blockUploadName: { default: '' }, + blockUploadShowPreview: { default: true }, + blockUploadType: { + default: '', + }, + blockUploadUrl: { default: '' }, }, content: 'none', }, { - render: ({ block }) => { - return ( - - {block.props.type === 'warning' ? ( - - ) : ( - - )} - {block.props.information} - - ); - }, + render: (props) => , }, ); 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 d8a0878f..461b6fd7 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 @@ -1,36 +1,11 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { backendUrl } from '@/api'; -import { useMediaUrl } from '@/core/config'; -import { sleep } from '@/utils'; -import { checkDocMediaStatus, useCreateDocAttachment } from '../api'; +import { useCreateDocAttachment } from '../api'; import { DocsBlockNoteEditor } from '../types'; -/** - * Upload file can be analyzed on the server side, - * we had this function to wait for the analysis to be done - * before returning the file url. It will keep the loader - * on the upload button until the analysis is done. - * @param url - * @returns Promise status_code - * @description Waits for the upload to be analyzed by checking the status of the file. - */ -const loopCheckDocMediaStatus = async (url: string) => { - const SLEEP_TIME = 5000; - const response = await checkDocMediaStatus({ - urlMedia: url, - }); - - if (response.status === 'ready') { - return response; - } else { - await sleep(SLEEP_TIME); - return await loopCheckDocMediaStatus(url); - } -}; - export const useUploadFile = (docId: string) => { const { mutateAsync: createDocAttachment, @@ -63,91 +38,6 @@ export const useUploadFile = (docId: string) => { export const useUploadStatus = (editor: DocsBlockNoteEditor) => { const ANALYZE_URL = 'media-check'; const { t } = useTranslation(); - const mediaUrl = useMediaUrl(); - const timeoutIds = useRef>({}); - - const blockAnalyzeProcess = useCallback( - (editor: DocsBlockNoteEditor, blockId: string, url: string) => { - if (timeoutIds.current[url]) { - clearTimeout(timeoutIds.current[url]); - } - - // Delay to let the time to the dom to be rendered - const timoutId = setTimeout(() => { - // 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) => { - if (insertedBlocks.length === 0 || removedBlocks.length === 0) { - return; - } - - const loadingBlockId = insertedBlocks[0].id; - const removedBlock = removedBlocks[0]; - - removedBlock.props = { - ...removedBlock.props, - url: `${mediaUrl}${response.file}`, - }; - - // Replace the loading block with the resource block (image, audio, video, pdf ...) - editor.replaceBlocks([loadingBlockId], [removedBlock]); - }) - .catch((error) => { - console.error('Error analyzing file:', error); - - 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); - - timeoutIds.current[url] = timoutId; - }, - [t, mediaUrl], - ); - - useEffect(() => { - const blocksAnalyze = editor?.document.filter( - (block) => 'url' in block.props && block.props.url.includes(ANALYZE_URL), - ); - - if (!blocksAnalyze?.length) { - return; - } - - blocksAnalyze.forEach((block) => { - if (!('url' in block.props)) { - return; - } - - blockAnalyzeProcess(editor, block.id, block.props.url); - }); - }, [blockAnalyzeProcess, editor]); useEffect(() => { editor.onChange((_, context) => { @@ -169,11 +59,38 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => { return; } - blockAnalyzeProcess( - editor, - blockChanges.block.id, - blockChanges.block.props.url, - ); + const blockUploadUrl = blockChanges.block.props.url; + const blockUploadType = blockChanges.block.type; + const blockUploadName = blockChanges.block.props.name; + const blockUploadShowPreview = + ('showPreview' in blockChanges.block.props && + blockChanges.block.props.showPreview) || + false; + + const timeoutId = setTimeout(() => { + // Replace the resource block by a uploadLoader block + // to show analyzing status + editor.replaceBlocks( + [blockChanges.block.id], + [ + { + type: 'uploadLoader', + props: { + information: t('Analyzing file...'), + type: 'loading', + blockUploadName, + blockUploadType, + blockUploadUrl, + blockUploadShowPreview, + }, + }, + ], + ); + }, 250); + + return () => { + clearTimeout(timeoutId); + }; }); - }, [blockAnalyzeProcess, mediaUrl, editor, t]); + }, [editor, t]); };