🥅(frontend) change ui when catch unexpected upload error

We updated the UI to provide a clearer indication
when an unexpected upload error occurs.
Before the error was displayed on the top
of the editor, making it less noticeable to the user,
now we display the error directly within the
upload block, making it more visible to the user
and more user-friendly.
This commit is contained in:
Anthony LC
2026-10-16 11:23:52 +02:00
parent e76dc941bd
commit aff00ca5b6
4 changed files with 57 additions and 31 deletions
@@ -356,20 +356,20 @@ test.describe('Doc Editor', () => {
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
await expect(editor.getByText('Analyzing file...')).toBeVisible();
await expect(editor.getByText('Analyzing the file')).toBeVisible();
// To be sure the retry happens even after a page reload
await goToGridDoc(page, {
title: randomDoc,
});
await expect(editor.getByText('Analyzing file...')).toBeVisible();
await expect(editor.getByText('Analyzing the file')).toBeVisible();
// The retry takes a few seconds
await expect(editor.getByText('test.html')).toBeVisible({
timeout: 7000,
});
await expect(editor.getByText('Analyzing file...')).toBeHidden();
await expect(editor.getByText('Analyzing the file')).toBeHidden();
});
test('it checks if callout custom block', async ({ page, browserName }) => {
@@ -23,6 +23,10 @@ import {
import {
FloatingComposerController,
FloatingThreadController,
ReactAudioBlock,
ReactFileBlock,
ReactImageBlock,
ReactVideoBlock,
ThreadsSidebar,
useCreateBlockNote,
} from '@blocknote/react';
@@ -33,7 +37,7 @@ import { useTranslation } from 'react-i18next';
import { WebsocketProvider } from 'y-websocket';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
import { Box } from '@/components';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import {
@@ -83,6 +87,10 @@ const baseBlockNoteSchema = withPageBreak(
BlockNoteSchema.create({
blockSpecs: {
...defaultBlockSpecs,
audio: ReactAudioBlock(),
file: ReactFileBlock(),
image: ReactImageBlock(),
video: ReactVideoBlock(),
callout: CalloutBlock(),
codeBlock: createSafeCodeBlockSpec(),
diagram: createReactDiagramBlockSpec(),
@@ -133,7 +141,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
? DEFAULT_LOCALE
: i18n.resolvedLanguage;
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
const { uploadFile } = useUploadFile(doc.id);
const conf = useConfig().data;
const { isFeatureFlagActivated } = useAnalytics();
const aiBlockNoteAllowed = !!(
@@ -346,15 +354,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
currentUserAvatarUrl={currentUserAvatarUrl}
/>
<DocsFindReplaceStyle />
{errorAttachment && (
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
<TextErrors
causes={errorAttachment.cause}
canClose
$textAlign="left"
/>
</Box>
)}
<BlockNoteView
className="--docs--main-editor"
editor={editor}
@@ -71,6 +71,26 @@ describe('useUploadFile', () => {
'error',
);
});
it('toasts the causes of the API and rethrows when the upload fails', async () => {
fetchMock.hardReset();
fetchMock.mockGlobal();
fetchMock.post(uploadUrl, {
status: 400,
body: { file: ['File type not allowed.'] },
});
const result = renderUseUploadFile();
await expect(
result.current.uploadFile(createFile(MAX_FILE_SIZE)),
).rejects.toThrow('Failed to upload on the doc');
expect(mockToast).toHaveBeenCalledWith(
'A problem occurred while uploading the file, cause: File type not allowed.',
'error',
);
});
});
describe('useUploadStatus', () => {
@@ -4,7 +4,7 @@ import { captureException } from '@sentry/nextjs';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { backendUrl } from '@/api';
import { APIError, backendUrl } from '@/api';
import { useConfig } from '@/core';
import { useToast } from '@/hooks';
import { formatFileSize } from '@/utils';
@@ -44,11 +44,7 @@ export const useUploadFile = (docId: string) => {
const { t } = useTranslation();
const { toast } = useToast();
const { data: config } = useConfig();
const {
mutateAsync: createDocAttachment,
isError: isErrorAttachment,
error: errorAttachment,
} = useCreateDocAttachment();
const { mutateAsync: createDocAttachment } = useCreateDocAttachment();
const maxFileSize = config?.DOCUMENT_IMAGE_MAX_SIZE ?? DEFAULT_MAX_FILE_SIZE;
@@ -75,21 +71,32 @@ export const useUploadFile = (docId: string) => {
const body = new FormData();
body.append('file', file);
const ret = await createDocAttachment({
docId,
body,
});
try {
const ret = await createDocAttachment({
docId,
body,
});
return `${backendUrl()}${ret.file}`;
return `${backendUrl()}${ret.file}`;
} catch (error) {
const causes = error instanceof APIError ? error.cause : undefined;
toast(
causes?.length
? `${t('A problem occurred while uploading the file, cause:')} ${causes.join(' ')}`
: t(
'A problem occurred while uploading the file, please try again.',
),
VariantType.ERROR,
);
throw error;
}
},
[createDocAttachment, docId, maxFileSize, t, toast],
);
return {
uploadFile,
isErrorAttachment,
errorAttachment,
};
return { uploadFile };
};
/**
@@ -132,7 +139,7 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
{
type: 'uploadLoader',
props: {
information: t('Analyzing file...'),
information: t('Analyzing the file'),
type: 'loading',
blockUploadName,
blockUploadType,