From 58260b19dcea74816774d23db890f2136eafbd48 Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Mon, 13 Jul 2026 16:04:06 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=94=A7(backend)=20expose=20DOCUMENT?= =?UTF-8?q?=5FIMAGE=5FMAX=5FSIZE=20in=20config=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend needs this value to validate the file size before upload, to display a nice error message instead of the cryptic 413 error when Nginx rejects files over 10MB. Signed-off-by: Mathieu Agopian --- src/backend/core/api/viewsets.py | 1 + src/backend/core/tests/test_api_config.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 5d9991bcbf..4d7069ef89 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -3085,6 +3085,7 @@ def get(self, request): "CONVERSION_FILE_EXTENSIONS_ALLOWED", "CONVERSION_FILE_MAX_SIZE", "CONVERSION_UPLOAD_ENABLED", + "DOCUMENT_IMAGE_MAX_SIZE", "ENVIRONMENT", "FRONTEND_CSS_URL", "FRONTEND_HOMEPAGE_FEATURE_ENABLED", diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index 5f7fef4536..192a3cc2f5 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -61,6 +61,7 @@ def test_api_config(is_authenticated): "CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"], "CONVERSION_FILE_MAX_SIZE": 20971520, "CONVERSION_UPLOAD_ENABLED": False, + "DOCUMENT_IMAGE_MAX_SIZE": 10485760, "ENVIRONMENT": "test", "FRONTEND_CSS_URL": "http://testcss/", "FRONTEND_HOMEPAGE_FEATURE_ENABLED": True, From ee82d4ec4cb18dc597c28ba30cbe1c8175dec9e0 Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Mon, 13 Jul 2026 17:28:51 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B(frontend)=20handle=20non-JSON?= =?UTF-8?q?=20error=20responses=20in=20errorCauses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nginx rejects files over 10MB with a 413 and an HTML body. Parsing it as JSON throws a SyntaxError, swallowing the error and showing no feedback to the user. Signed-off-by: Mathieu Agopian --- .../apps/impress/src/api/__tests__/utils.test.ts | 14 ++++++++++++++ src/frontend/apps/impress/src/api/utils.ts | 10 ++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/frontend/apps/impress/src/api/__tests__/utils.test.ts b/src/frontend/apps/impress/src/api/__tests__/utils.test.ts index f4f61bad8b..3d3ec9e449 100644 --- a/src/frontend/apps/impress/src/api/__tests__/utils.test.ts +++ b/src/frontend/apps/impress/src/api/__tests__/utils.test.ts @@ -35,6 +35,20 @@ describe('utils', () => { expect(result.cause).toBeUndefined(); expect(result.data).toBeUndefined(); }); + + it('returns undefined causes when response body is not valid JSON (e.g. 413 from Nginx)', async () => { + const mockResponse = { + status: 413, + json: () => + Promise.reject(new SyntaxError('Unexpected token < in JSON')), + } as unknown as Response; + + const result = await errorCauses(mockResponse); + + expect(result.status).toBe(413); + expect(result.cause).toBeUndefined(); + expect(result.data).toBeUndefined(); + }); }); describe('getCSRFToken', () => { diff --git a/src/frontend/apps/impress/src/api/utils.ts b/src/frontend/apps/impress/src/api/utils.ts index 82bbe505ae..0225c3fcfd 100644 --- a/src/frontend/apps/impress/src/api/utils.ts +++ b/src/frontend/apps/impress/src/api/utils.ts @@ -12,10 +12,12 @@ * - `data`: The optional data passed in */ export const errorCauses = async (response: Response, data?: unknown) => { - const errorsBody = (await response.json()) as Record< - string, - string | string[] - > | null; + let errorsBody: Record | null = null; + try { + errorsBody = await response.json(); + } catch { + // response body is not JSON (e.g. HTML error page from Nginx) + } const causes = errorsBody ? Object.entries(errorsBody) From 77f63b1ad57d192d92d2895024e20409c872cb96 Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Mon, 13 Jul 2026 18:03:25 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8(frontend)=20validate=20file=20siz?= =?UTF-8?q?e=20before=20upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check the file size client-side before attempting the upload and throw a clear APIError instead. Signed-off-by: Mathieu Agopian --- .../impress/src/core/config/api/useConfig.tsx | 1 + .../hook/__tests__/useUploadFile.test.tsx | 60 +++++++++++++++++++ .../docs/doc-editor/hook/useUploadFile.tsx | 17 +++++- 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index b204e5a381..aa5ef10cd9 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -54,6 +54,7 @@ export interface ConfigResponse { CONVERSION_FILE_EXTENSIONS_ALLOWED: string[]; CONVERSION_FILE_MAX_SIZE: number; CONVERSION_UPLOAD_ENABLED?: boolean; + DOCUMENT_IMAGE_MAX_SIZE?: number; ENVIRONMENT: string; FRONTEND_CSS_URL?: string; FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx new file mode 100644 index 0000000000..89235b40b2 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx @@ -0,0 +1,60 @@ +import { renderHook } from '@testing-library/react'; +import fetchMock from 'fetch-mock'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { APIError } from '@/api'; +import { AppWrapper } from '@/tests/utils'; + +import { useUploadFile } from '../useUploadFile'; + +vi.mock('@/core', () => ({ + useConfig: () => ({ + data: { DOCUMENT_IMAGE_MAX_SIZE: 1 }, // 1 byte limit for test + }), +})); + +describe('useUploadFile', () => { + // Fake file with a size slightly under the limit + const smallFile = new File( + [new ArrayBuffer(1 * 1024 * 1024 - 1)], + 'big.png', + { + type: 'image/png', + }, + ); + // Fake file with a size slightly over the limit + const bigFile = new File([new ArrayBuffer(1 * 1024 * 1024 + 1)], 'big.png', { + type: 'image/png', + }); + + beforeEach(() => { + fetchMock.restore(); + }); + + it("proceeds to upload when file doesn't exceed the size limit", async () => { + fetchMock.post( + 'http://test.jest/api/v1.0/documents/doc-id/attachment-upload/', + { body: { file: '/media/test.jpg' } }, + ); + const { result } = renderHook(() => useUploadFile('doc-id'), { + wrapper: AppWrapper, + }); + + await result.current.uploadFile(smallFile); + expect(fetchMock.calls()).toHaveLength(1); + }); + + it('throws an APIError before uploading when file exceeds the size limit', async () => { + const { result } = renderHook(() => useUploadFile('doc-id'), { + wrapper: AppWrapper, + }); + + // Fake 2 bytes file + const bigFile = new File([new ArrayBuffer(2)], 'big.png', { + type: 'image/png', + }); + + await expect(result.current.uploadFile(bigFile)).rejects.toThrow(APIError); + expect(fetchMock.calls()).toHaveLength(0); + }); +}); 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 5913f2d559..4f008fdb52 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 @@ -3,7 +3,8 @@ 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 { isSafeUrl } from '@/utils/url'; import { useCreateDocAttachment } from '../api'; @@ -11,6 +12,8 @@ import { ANALYZE_URL } from '../conf'; import { DocsBlockNoteEditor } from '../types'; export const useUploadFile = (docId: string) => { + const { t } = useTranslation(); + const { data: config } = useConfig(); const { mutateAsync: createDocAttachment, isError: isErrorAttachment, @@ -19,6 +22,18 @@ export const useUploadFile = (docId: string) => { const uploadFile = useCallback( async (file: File) => { + const maxSize = config?.DOCUMENT_IMAGE_MAX_SIZE ?? 10 * 1024 * 1024; // Default to 10MB if config isn't provided by the backend. + if (file.size > maxSize) { + throw new APIError(t('File is too large'), { + status: 413, // Replicate what Nginx answers when dealing with a file too big. + cause: [ + t('File size exceeds the maximum allowed size of {{size}}MB.', { + size: Math.round(maxSize / (1024 * 1024)), + }), + ], + }); + } + const body = new FormData(); body.append('file', file); From 1097e43120361a553be1ea644ce852e0e5557eb4 Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Mon, 13 Jul 2026 19:15:47 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20display=20an=20err?= =?UTF-8?q?or=20message=20when=20the=20uploaded=20file=20is=20too=20big?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set the error state if the file is too big so the UI displays a clear message, and then throw so BlockNote aborts the upload. Signed-off-by: Mathieu Agopian --- .../hook/__tests__/useUploadFile.test.tsx | 25 +++++++++++++------ .../docs/doc-editor/hook/useUploadFile.tsx | 12 ++++++--- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx index 89235b40b2..34180b7b01 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react'; +import { renderHook, waitFor } from '@testing-library/react'; import fetchMock from 'fetch-mock'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -9,7 +9,7 @@ import { useUploadFile } from '../useUploadFile'; vi.mock('@/core', () => ({ useConfig: () => ({ - data: { DOCUMENT_IMAGE_MAX_SIZE: 1 }, // 1 byte limit for test + data: { DOCUMENT_IMAGE_MAX_SIZE: 1 * 1024 * 1024 }, // 1MB limit for test }), })); @@ -49,12 +49,23 @@ describe('useUploadFile', () => { wrapper: AppWrapper, }); - // Fake 2 bytes file - const bigFile = new File([new ArrayBuffer(2)], 'big.png', { - type: 'image/png', - }); - await expect(result.current.uploadFile(bigFile)).rejects.toThrow(APIError); expect(fetchMock.calls()).toHaveLength(0); }); + + it('sets errorAttachment with a user-friendly message when file exceeds the size limit', async () => { + const { result } = renderHook(() => useUploadFile('doc-id'), { + wrapper: AppWrapper, + }); + + await result.current.uploadFile(bigFile).catch(() => {}); + + await waitFor(() => { + expect(result.current.isErrorAttachment).toBe(true); + }); + + expect(result.current.errorAttachment?.cause).toEqual([ + 'File size exceeds the maximum allowed size of 1MB.', + ]); + }); }); 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 4f008fdb52..9539a96b85 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,6 +1,6 @@ import { Block } from '@blocknote/core'; import { captureException } from '@sentry/nextjs'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { APIError, backendUrl } from '@/api'; @@ -14,6 +14,7 @@ import { DocsBlockNoteEditor } from '../types'; export const useUploadFile = (docId: string) => { const { t } = useTranslation(); const { data: config } = useConfig(); + const [sizeError, setSizeError] = useState(null); const { mutateAsync: createDocAttachment, isError: isErrorAttachment, @@ -24,7 +25,7 @@ export const useUploadFile = (docId: string) => { async (file: File) => { const maxSize = config?.DOCUMENT_IMAGE_MAX_SIZE ?? 10 * 1024 * 1024; // Default to 10MB if config isn't provided by the backend. if (file.size > maxSize) { - throw new APIError(t('File is too large'), { + const error = new APIError(t('File is too large'), { status: 413, // Replicate what Nginx answers when dealing with a file too big. cause: [ t('File size exceeds the maximum allowed size of {{size}}MB.', { @@ -32,7 +33,10 @@ export const useUploadFile = (docId: string) => { }), ], }); + setSizeError(error); + throw error; } + setSizeError(null); const body = new FormData(); body.append('file', file); @@ -49,8 +53,8 @@ export const useUploadFile = (docId: string) => { return { uploadFile, - isErrorAttachment, - errorAttachment, + isErrorAttachment: isErrorAttachment || !!sizeError, + errorAttachment: sizeError ?? errorAttachment, }; }; From 23f25a238566b3e554a77fb789c62e563ac6df3e Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Tue, 14 Jul 2026 10:56:12 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20handle=20the=20upl?= =?UTF-8?q?oad=20of=20a=20too=20big=20file=20in=20BlockNoteEditor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uploading a too big file displays an error message to the user, and we need to make sure that - the "loading" node isn't left over - when hiding the error message, and re-uploading the too big file, the error message displays again Signed-off-by: Mathieu Agopian --- .../doc-editor/components/BlockNoteEditor.tsx | 28 +++++++++++++++++-- .../hook/__tests__/useUploadFile.test.tsx | 24 ++++++++++++++++ .../docs/doc-editor/hook/useUploadFile.tsx | 17 +++++++++-- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 5c9b2b1e29..8465666409 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -111,7 +111,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { ? DEFAULT_LOCALE : i18n.resolvedLanguage; - const { uploadFile, errorAttachment } = useUploadFile(doc.id); + const { uploadFile, checkFileSize, errorAttachment, sizeErrorKey } = + useUploadFile(doc.id); const conf = useConfig().data; const { isFeatureFlagActivated } = useAnalytics(); const aiBlockNoteAllowed = !!( @@ -263,6 +264,26 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { useUploadStatus(editor); + useEffect(() => { + const container = refEditorContainer.current; + if (!container) return; + + const handleDrop = (event: DragEvent) => { + const files = Array.from(event.dataTransfer?.files ?? []); + try { + files.forEach(checkFileSize); + } catch { + event.stopPropagation(); + event.preventDefault(); + } + }; + + container.addEventListener('drop', handleDrop, true); + return () => { + container.removeEventListener('drop', handleDrop, true); + }; + }, [checkFileSize]); + useEffect(() => { setEditor(editor); @@ -279,7 +300,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { currentUserAvatarUrl={currentUserAvatarUrl} /> {errorAttachment && ( - + { 'File size exceeds the maximum allowed size of 1MB.', ]); }); + + it('exposes checkFileSize that does not throw for files within the limit', () => { + const { result } = renderHook(() => useUploadFile('doc-id'), { + wrapper: AppWrapper, + }); + + expect(() => result.current.checkFileSize(smallFile)).not.toThrow(); + }); + + it('exposes checkFileSize that throws and sets errorAttachment for files over the limit', async () => { + const { result } = renderHook(() => useUploadFile('doc-id'), { + wrapper: AppWrapper, + }); + + expect(() => result.current.checkFileSize(bigFile)).toThrow(APIError); + + await waitFor(() => { + expect(result.current.isErrorAttachment).toBe(true); + }); + + expect(result.current.errorAttachment?.cause).toEqual([ + 'File size exceeds the maximum allowed size of 1MB.', + ]); + }); }); 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 9539a96b85..f8021c500b 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 @@ -15,14 +15,15 @@ export const useUploadFile = (docId: string) => { const { t } = useTranslation(); const { data: config } = useConfig(); const [sizeError, setSizeError] = useState(null); + const [sizeErrorKey, setSizeErrorKey] = useState(0); const { mutateAsync: createDocAttachment, isError: isErrorAttachment, error: errorAttachment, } = useCreateDocAttachment(); - const uploadFile = useCallback( - async (file: File) => { + const checkFileSize = useCallback( + (file: File) => { const maxSize = config?.DOCUMENT_IMAGE_MAX_SIZE ?? 10 * 1024 * 1024; // Default to 10MB if config isn't provided by the backend. if (file.size > maxSize) { const error = new APIError(t('File is too large'), { @@ -34,9 +35,17 @@ export const useUploadFile = (docId: string) => { ], }); setSizeError(error); + setSizeErrorKey((prev) => prev + 1); throw error; } setSizeError(null); + }, + [config?.DOCUMENT_IMAGE_MAX_SIZE, setSizeError, setSizeErrorKey, t], + ); + + const uploadFile = useCallback( + async (file: File) => { + checkFileSize(file); const body = new FormData(); body.append('file', file); @@ -48,13 +57,15 @@ export const useUploadFile = (docId: string) => { return `${backendUrl()}${ret.file}`; }, - [createDocAttachment, docId], + [checkFileSize, createDocAttachment, docId], ); return { uploadFile, + checkFileSize, isErrorAttachment: isErrorAttachment || !!sizeError, errorAttachment: sizeError ?? errorAttachment, + sizeErrorKey, }; }; From 691955ec62d3b1bedd2bcc6365908e1e08085859 Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Tue, 14 Jul 2026 12:05:45 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=85(frontend)=20add=20e2e=20test=20fo?= =?UTF-8?q?r=20too=20big=20file=20uploads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mathieu Agopian --- .../app-impress/doc-editor-upload.spec.ts | 67 +++++++++++++++++++ .../e2e/__tests__/app-impress/utils-common.ts | 1 + 2 files changed, 68 insertions(+) create mode 100644 src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts new file mode 100644 index 0000000000..6c10bc69f2 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts @@ -0,0 +1,67 @@ +import { Page, expect, test } from '@playwright/test'; + +import { createDoc, overrideConfig } from './utils-common'; +import { getEditor } from './utils-editor'; + +const dropFileInEditor = async (page: Page) => { + const dataTransfer = await page.evaluateHandle(() => { + const dt = new DataTransfer(); + // 1MB + 1 byte, exceeds the 1MB limit set in overrideConfig. + const file = new File([new Uint8Array(1048577)], 'video.mp4', { + type: 'video/mp4', + }); + dt.items.add(file); + return dt; + }); + + await page + .getByLabel('Document editor') + .dispatchEvent('drop', { dataTransfer }); +}; + +test.describe('Doc Editor - File Upload', () => { + test('dropping a file that is too large shows an error and does not leave a loading block', async ({ + page, + browserName, + }) => { + await overrideConfig(page, { DOCUMENT_IMAGE_MAX_SIZE: 1048576 }); // Override the size limit to 1MB for the test. + await page.goto('/'); + await createDoc(page, 'doc-upload-too-large', browserName, 1); + await getEditor({ page }); + + await dropFileInEditor(page); + + await expect( + page.getByText('File size exceeds the maximum allowed size of 1MB.'), + ).toBeVisible(); + + await expect(page.getByText('Loading...')).toBeHidden(); + }); + + test('dismissing the error and dropping the same file again shows the error again', async ({ + page, + browserName, + }) => { + await overrideConfig(page, { DOCUMENT_IMAGE_MAX_SIZE: 1048576 }); // Override the size limit to 1MB for the test. + await page.goto('/'); + await createDoc(page, 'doc-upload-error-retry', browserName, 1); + await getEditor({ page }); + + await dropFileInEditor(page); + await expect( + page.getByText('File size exceeds the maximum allowed size of 1MB.'), + ).toBeVisible(); + + // Dismiss the error + await page.locator('.--docs--text-errors').getByRole('button').click(); + await expect( + page.getByText('File size exceeds the maximum allowed size of 1MB.'), + ).toBeHidden(); + + // Drop the same file again + await dropFileInEditor(page); + await expect( + page.getByText('File size exceeds the maximum allowed size of 1MB.'), + ).toBeVisible(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index b42785719e..92c8eac061 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -24,6 +24,7 @@ export const CONFIG = { CONVERSION_UPLOAD_ENABLED: true, CONVERSION_FILE_EXTENSIONS_ALLOWED: ['.docx', '.md'], CONVERSION_FILE_MAX_SIZE: 20971520, + DOCUMENT_IMAGE_MAX_SIZE: 10485760, ENVIRONMENT: 'development', FRONTEND_CSS_URL: null, FRONTEND_JS_URL: null, From 60a3aae0ad6f4f28136028651e099e4e6b770c16 Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Tue, 14 Jul 2026 15:28:35 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20handle=20the=20pas?= =?UTF-8?q?ting=20of=20a=20too=20big=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same way it's checking before uploading a file using the drag and drop, it also checks if it's via copy/paste. Signed-off-by: Mathieu Agopian --- .../app-impress/doc-editor-upload.spec.ts | 41 +++++++++++++++++++ .../doc-editor/components/BlockNoteEditor.tsx | 22 ++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts index 6c10bc69f2..c4a9bfe0d9 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor-upload.spec.ts @@ -19,6 +19,25 @@ const dropFileInEditor = async (page: Page) => { .dispatchEvent('drop', { dataTransfer }); }; +const pasteFileInEditor = async (page: Page) => { + await page.getByLabel('Document editor').focus(); + + await page.getByLabel('Document editor').evaluate((el) => { + const dt = new DataTransfer(); + const file = new File([new Uint8Array(1048577)], 'video.mp4', { + type: 'video/mp4', + }); + dt.items.add(file); + + const event = new ClipboardEvent('paste', { + clipboardData: dt, + bubbles: true, + cancelable: true, + }); + el.dispatchEvent(event); + }); +}; + test.describe('Doc Editor - File Upload', () => { test('dropping a file that is too large shows an error and does not leave a loading block', async ({ page, @@ -64,4 +83,26 @@ test.describe('Doc Editor - File Upload', () => { page.getByText('File size exceeds the maximum allowed size of 1MB.'), ).toBeVisible(); }); + + test('pasting a file that is too large shows an error and does not leave a loading block', async ({ + page, + browserName, + }) => { + test.skip( + browserName === 'firefox', + 'Firefox does not expose clipboardData.items on synthetic ClipboardEvents, making this untestable via dispatchEvent.', + ); + await overrideConfig(page, { DOCUMENT_IMAGE_MAX_SIZE: 1048576 }); + await page.goto('/'); + await createDoc(page, 'doc-upload-paste-too-large', browserName, 1); + await getEditor({ page }); + + await pasteFileInEditor(page); + + await expect( + page.getByText('File size exceeds the maximum allowed size of 1MB.'), + ).toBeVisible(); + + await expect(page.getByText('Loading...')).toBeHidden(); + }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 8465666409..0a359e2574 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -207,6 +207,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { }), }, pasteHandler: ({ event, defaultPasteHandler }) => { + const files = Array.from(event.clipboardData?.files ?? []); + try { + files.forEach(checkFileSize); + } catch { + return; + } + // Get clipboard data const blocknoteData = event.clipboardData?.getData('blocknote/html'); @@ -278,9 +285,24 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { } }; + const handlePaste = (event: ClipboardEvent) => { + const files = Array.from(event.clipboardData?.items ?? []) + .filter((item) => item.kind === 'file') + .map((item) => item.getAsFile()) + .filter((f): f is File => f !== null); + try { + files.forEach(checkFileSize); + } catch { + event.stopPropagation(); + event.preventDefault(); + } + }; + container.addEventListener('drop', handleDrop, true); + container.addEventListener('paste', handlePaste, true); return () => { container.removeEventListener('drop', handleDrop, true); + container.removeEventListener('paste', handlePaste, true); }; }, [checkFileSize]); From 6822be9e1f89a34b5b132b9df05c2e3051d82b8f Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Tue, 14 Jul 2026 13:59:17 +0200 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=93=9D(frontend)=20update=20the=20CHA?= =?UTF-8?q?NGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mathieu Agopian --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aafeb4ab4..ba74cb5140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Added - ♿️(frontend) restore skip to content link after header redesign #2510 +- ✨(frontend) warn the user when trying to upload a file size that exceeds the limit #2522 ## [v5.4.1] - 2026-07-09