🐛(frontend) retry check media status after page reload
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.
This commit is contained in:
@@ -24,6 +24,7 @@ and this project adheres to
|
|||||||
- ♿(frontend) improve accessibility:
|
- ♿(frontend) improve accessibility:
|
||||||
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
|
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
|
||||||
- 🐛(backend) fix s3 version_id validation
|
- 🐛(backend) fix s3 version_id validation
|
||||||
|
- 🐛(frontend) retry check media status after page reload #1555
|
||||||
|
|
||||||
## [3.8.2] - 2025-10-17
|
## [3.8.2] - 2025-10-17
|
||||||
|
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ test.describe('Doc Editor', () => {
|
|||||||
if (request.method().includes('GET')) {
|
if (request.method().includes('GET')) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
json: {
|
json: {
|
||||||
status: requestCount ? 'ready' : 'processing',
|
status: requestCount > 1 ? 'ready' : 'processing',
|
||||||
file: '/anything.html',
|
file: '/anything.html',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -518,6 +518,12 @@ test.describe('Doc Editor', () => {
|
|||||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
|
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
|
||||||
|
|
||||||
await expect(editor.getByText('Analyzing file...')).toBeVisible();
|
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
|
// The retry takes a few seconds
|
||||||
await expect(editor.getByText('test.html')).toBeVisible({
|
await expect(editor.getByText('test.html')).toBeVisible({
|
||||||
timeout: 7000,
|
timeout: 7000,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { APIError, errorCauses } from '@/api';
|
import { APIError, errorCauses } from '@/api';
|
||||||
|
import { sleep } from '@/utils';
|
||||||
|
|
||||||
interface CheckDocMediaStatusResponse {
|
interface CheckDocMediaStatusResponse {
|
||||||
file?: string;
|
file?: string;
|
||||||
@@ -25,3 +26,28 @@ export const checkDocMediaStatus = async ({
|
|||||||
|
|
||||||
return response.json() as Promise<CheckDocMediaStatusResponse>;
|
return response.json() as Promise<CheckDocMediaStatusResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<CheckDocMediaStatusResponse> status_code
|
||||||
|
* @description Waits for the upload to be analyzed by checking the status of the file.
|
||||||
|
*/
|
||||||
|
export const loopCheckDocMediaStatus = async (
|
||||||
|
url: string,
|
||||||
|
): Promise<CheckDocMediaStatusResponse> => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ type CreatePDFBlockConfig = BlockConfig<
|
|||||||
|
|
||||||
interface PdfBlockComponentProps {
|
interface PdfBlockComponentProps {
|
||||||
block: BlockNoDefaults<
|
block: BlockNoDefaults<
|
||||||
Record<'callout', CreatePDFBlockConfig>,
|
Record<'pdf', CreatePDFBlockConfig>,
|
||||||
InlineContentSchema,
|
InlineContentSchema,
|
||||||
StyleSchema
|
StyleSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -1,34 +1,138 @@
|
|||||||
|
import {
|
||||||
|
BlockConfig,
|
||||||
|
BlockNoDefaults,
|
||||||
|
BlockNoteEditor,
|
||||||
|
InlineContentSchema,
|
||||||
|
StyleSchema,
|
||||||
|
} from '@blocknote/core';
|
||||||
import { createReactBlockSpec } from '@blocknote/react';
|
import { createReactBlockSpec } from '@blocknote/react';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, Text } from '@/components';
|
||||||
|
import { useMediaUrl } from '@/core';
|
||||||
|
|
||||||
|
import { loopCheckDocMediaStatus } from '../../api';
|
||||||
import Loader from '../../assets/loader.svg';
|
import Loader from '../../assets/loader.svg';
|
||||||
import Warning from '../../assets/warning.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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const UploadLoaderBlock = createReactBlockSpec(
|
export const UploadLoaderBlock = createReactBlockSpec(
|
||||||
{
|
{
|
||||||
type: 'uploadLoader',
|
type: 'uploadLoader',
|
||||||
propSchema: {
|
propSchema: {
|
||||||
information: { default: '' as const },
|
information: { default: '' },
|
||||||
type: {
|
type: {
|
||||||
default: 'loading' as const,
|
default: 'loading',
|
||||||
values: ['loading', 'warning'] as const,
|
values: ['loading', 'warning'],
|
||||||
},
|
},
|
||||||
|
blockUploadName: { default: '' },
|
||||||
|
blockUploadShowPreview: { default: true },
|
||||||
|
blockUploadType: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
blockUploadUrl: { default: '' },
|
||||||
},
|
},
|
||||||
content: 'none',
|
content: 'none',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
render: ({ block }) => {
|
render: (props) => <UploadLoaderBlockComponent {...props} />,
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,11 @@
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { backendUrl } from '@/api';
|
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';
|
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<CheckDocMediaStatusResponse> 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) => {
|
export const useUploadFile = (docId: string) => {
|
||||||
const {
|
const {
|
||||||
mutateAsync: createDocAttachment,
|
mutateAsync: createDocAttachment,
|
||||||
@@ -63,91 +38,6 @@ export const useUploadFile = (docId: string) => {
|
|||||||
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||||
const ANALYZE_URL = 'media-check';
|
const ANALYZE_URL = 'media-check';
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mediaUrl = useMediaUrl();
|
|
||||||
const timeoutIds = useRef<Record<string, NodeJS.Timeout>>({});
|
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
editor.onChange((_, context) => {
|
editor.onChange((_, context) => {
|
||||||
@@ -169,11 +59,38 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockAnalyzeProcess(
|
const blockUploadUrl = blockChanges.block.props.url;
|
||||||
editor,
|
const blockUploadType = blockChanges.block.type;
|
||||||
blockChanges.block.id,
|
const blockUploadName = blockChanges.block.props.name;
|
||||||
blockChanges.block.props.url,
|
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]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user