Repository: graasp/client
Pull request #995: fix: show error notifications on file upload
Author: login: pyphilia
Labels: name: v2.26.1
Created: 2025-05-08T08:25:55Z | Merged: 2025-05-12T11:27:43Z

The frontend shows the file-upload errors correctly:

- drop zone (modal, empty folder): shows the error in the dropzone
- drop in between rows: shows a notification

I guess it would be easier to have the same mechanism however we upload the file. So to move to notifications/progress dialog.


https://github.com/user-attachments/assets/27bc025d-99cc-4c72-a62f-46cf83124957


https://github.com/user-attachments/assets/cc2c5d48-d8df-4a8a-b8c4-df9dfaa3c32d


https://github.com/user-attachments/assets/945c1b95-00ef-4490-bdbb-732f62320af8




Diff:
diff --git a/cypress/e2e/player/hidden.cy.ts b/cypress/e2e/player/hidden.cy.ts
index 1ff713e2a..4b8def0ba 100644
--- a/cypress/e2e/player/hidden.cy.ts
+++ b/cypress/e2e/player/hidden.cy.ts
@@ -18,14 +18,18 @@ const parentItem = PackedFolderItemFactory({
 });
 const FOLDER_WITH_HIDDEN_ITEMS: ItemForTest[] = [
   parentItem,
-  DocumentItemFactory({ parentItem }),
-  PackedDocumentItemFactory({ parentItem }, { hiddenVisibility: {} }),
+  DocumentItemFactory({ parentItem, settings: { isCollapsible: false } }),
+  PackedDocumentItemFactory(
+    { parentItem, settings: { isCollapsible: false } },
+    { hiddenVisibility: {} },
+  ),
   PackedDocumentItemFactory(
     {
       name: 'hidden document',
       settings: {
         isPinned: false,
         showChatbox: false,
+        isCollapsible: false,
       },
       parentItem,
     },
diff --git a/src/config/notifier.ts b/src/config/notifier.ts
index 745c37c75..cefdb650c 100644
--- a/src/config/notifier.ts
+++ b/src/config/notifier.ts
@@ -1,6 +1,6 @@
 import { toast } from 'react-toastify';
 
-import { isAxiosError } from 'axios';
+import { AxiosError, isAxiosError } from 'axios';
 
 import { MessageKeys } from '@/@types/i18next';
 import { Notifier, routines } from '@/query';
@@ -76,3 +76,13 @@ export default ({
     toast.success(translate(message, { ns: NS.Messages }));
   }
 };
+
+export const getErrorMessage = (e: unknown) => {
+  if (e instanceof AxiosError) {
+    return e.response?.data?.message;
+  } else if (e instanceof Error) {
+    return e.message;
+  }
+
+  return null;
+};
diff --git a/src/locales/ar/builder.json b/src/locales/ar/builder.json
index e573d0514..ae2f24a9e 100644
--- a/src/locales/ar/builder.json
+++ b/src/locales/ar/builder.json
@@ -67,7 +67,6 @@
   "DOWNLOAD_ITEM_BUTTON": "تحميل",
   "DROP_DOWN_PLACEHOLDER": "الرجاء الاختيار من القائمة",
   "DROPZONE_HELPER_TEXT": "قم بإسقاط ملفاتك هنا للتحميل",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "الحد الأقصى 15 ملفًا بحجم 1 جيجابايت",
   "DROPZONE_HELPER_ACTION": "تصفح ملفات",
   "DROPZONE_HELPER_OPTIONAL_ACTION_TEXT": "أو",
   "DESCRIPTION_LABEL": "وصف",
diff --git a/src/locales/ar/messages.json b/src/locales/ar/messages.json
index c66543014..8ec26eb10 100644
--- a/src/locales/ar/messages.json
+++ b/src/locales/ar/messages.json
@@ -41,7 +41,6 @@
   "INVALID_DOWNLOAD_PARAMETERS": "واحد أو أكثر من معلمات التنزيل غير صالحة",
   "LOCAL_FILE_NOT_FOUND": "لم يتم العثور على الملف المحلي",
   "S3_FILE_NOT_FOUND": "لم يتم العثور على ملف S3",
-  "UPLOAD_FILE_UNEXPECTED_ERROR": "حدث خطأ غير متوقع أثناء تحميل ملف",
   "DOWNLOAD_FILE_UNEXPECTED_ERROR": "حدث خطأ غير متوقع أثناء تنزيل ملف",
   "RESTORE_ITEMS": "لقد نجحت في استعادة العنصر (العناصر)",
   "CREATE_ITEM": "تم إنشاء العنصر بنجاح",
diff --git a/src/locales/de/builder.json b/src/locales/de/builder.json
index 18e3cb341..1039569e7 100644
--- a/src/locales/de/builder.json
+++ b/src/locales/de/builder.json
@@ -67,7 +67,6 @@
   "DOWNLOAD_ITEM_BUTTON": "Download",
   "DROP_DOWN_PLACEHOLDER": "Bitte wählen Sie aus der Liste",
   "DROPZONE_HELPER_TEXT": "Legen Sie Ihre Dateien zum Hochladen hier ab",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Maximal 15 Dateien mit 1 GB",
   "DROPZONE_HELPER_ACTION": "Dateien durchsuchen",
   "DROPZONE_HELPER_OPTIONAL_ACTION_TEXT": "oder",
   "DESCRIPTION_LABEL": "Beschreibung",
diff --git a/src/locales/de/messages.json b/src/locales/de/messages.json
index c6797c905..8869ee793 100644
--- a/src/locales/de/messages.json
+++ b/src/locales/de/messages.json
@@ -41,7 +41,7 @@
   "INVALID_DOWNLOAD_PARAMETERS": "Einer oder mehrere Download-Parameter sind ungültig",
   "LOCAL_FILE_NOT_FOUND": "Die lokale Datei wurde nicht gefunden",
   "S3_FILE_NOT_FOUND": "Die S3-Datei wurde nicht gefunden",
-  "UPLOAD_FILE_UNEXPECTED_ERROR": "Beim Hochladen einer Datei ist ein unerwarteter Fehler aufgetreten",
+  "UPLOAD_FILE_UNEXPECTED_ERROR": "Beim Hochladen Dateien ist ein unerwarteter Fehler aufgetreten",
   "DOWNLOAD_FILE_UNEXPECTED_ERROR": "Beim Herunterladen einer Datei ist ein unerwarteter Fehler aufgetreten",
   "RESTORE_ITEMS": "Sie haben die Elemente erfolgreich wiederhergestellt",
   "CREATE_ITEM": "Das Element wurde erfolgreich erstellt",
diff --git a/src/locales/en/builder.json b/src/locales/en/builder.json
index 43ae91673..c893e4d5e 100644
--- a/src/locales/en/builder.json
+++ b/src/locales/en/builder.json
@@ -6,6 +6,9 @@
     "RECYCLED_ITEMS": "Trash",
     "RETURN_HOME": "Home"
   },
+  "FILE_UPLOADER": {
+    "GENERAL_ERROR": "Some files could not be uploaded, please fix the issue and try again."
+  },
   "REDIRECTION_TEXT": "Redirection",
   "REDIRECTION_BUTTON": "Navigate to redirection",
   "AVATAR_DEFAULT_ALT": "avatar",
@@ -69,7 +72,7 @@
   "DOWNLOAD_ITEM_BUTTON": "Download",
   "DROP_DOWN_PLACEHOLDER": "Please Choose From List",
   "DROPZONE_HELPER_TEXT": "Drop your files here to upload",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Max 15 files of 1GB",
+  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Max {{max}} files of 1GB",
   "DROPZONE_HELPER_TUTORIALS": "Where should you start? Click here for more information",
   "DROPZONE_HELPER_ACTION": "Browse Files",
   "DROPZONE_HELPER_OPTIONAL_ACTION_TEXT": "or",
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json
index fefb70d3b..220494bc3 100644
--- a/src/locales/en/messages.json
+++ b/src/locales/en/messages.json
@@ -41,7 +41,7 @@
   "INVALID_DOWNLOAD_PARAMETERS": "One or many download parameters are invalid",
   "LOCAL_FILE_NOT_FOUND": "The local file is not found",
   "S3_FILE_NOT_FOUND": "The S3 file is not found",
-  "UPLOAD_FILE_UNEXPECTED_ERROR": "An unexpected error happened while uploading a file",
+  "UPLOAD_FILES_UNEXPECTED_ERROR": "An unexpected error happened while uploading files",
   "DOWNLOAD_FILE_UNEXPECTED_ERROR": "An unexpected error happened while downloading a file",
   "RESTORE_ITEMS": "You successfully restored the item(s)",
   "CREATE_ITEM": "The item was successfully created",
diff --git a/src/locales/es/builder.json b/src/locales/es/builder.json
index b65527e80..98e8af7a3 100644
--- a/src/locales/es/builder.json
+++ b/src/locales/es/builder.json
@@ -58,7 +58,6 @@
   "DOWNLOAD_ITEM_BUTTON": "Descargar",
   "DROP_DOWN_PLACEHOLDER": "Por favor elija de la lista",
   "DROPZONE_HELPER_TEXT": "Deja tus archivos aquí para subirlos",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Máximo 15 archivos de 1 GB",
   "DROPZONE_HELPER_ACTION": "Búsqueda de archivos",
   "DROPZONE_HELPER_OPTIONAL_ACTION_TEXT": "o",
   "DESCRIPTION_LABEL": "Descripción",
diff --git a/src/locales/es/messages.json b/src/locales/es/messages.json
index a6fb67391..28e935760 100644
--- a/src/locales/es/messages.json
+++ b/src/locales/es/messages.json
@@ -41,7 +41,6 @@
   "INVALID_DOWNLOAD_PARAMETERS": "Uno o varios parámetros de descarga no son válidos",
   "LOCAL_FILE_NOT_FOUND": "No se encuentra el archivo local",
   "S3_FILE_NOT_FOUND": "No se encuentra el archivo S3",
-  "UPLOAD_FILE_UNEXPECTED_ERROR": "Se produjo un error inesperado al cargar un archivo",
   "DOWNLOAD_FILE_UNEXPECTED_ERROR": "Se produjo un error inesperado al descargar un archivo",
   "RESTORE_ITEMS": "Has restaurado exitosamente el(los) artículo(s)",
   "CREATE_ITEM": "El artículo se creó correctamente",
diff --git a/src/locales/fr/builder.json b/src/locales/fr/builder.json
index 82c896612..ae2eb14b7 100644
--- a/src/locales/fr/builder.json
+++ b/src/locales/fr/builder.json
@@ -6,6 +6,9 @@
     "RECYCLED_ITEMS": "Corbeille",
     "RETURN_HOME": "Accueil"
   },
+  "FILE_UPLOADER": {
+    "GENERAL_ERROR": "Certains fichiers n'ont pas pu être téléchargés, corrigez l'erreur et réessayez."
+  },
   "REDIRECTION_TEXT": "Redirection",
   "REDIRECTION_BUTTON": "Accéder à la redirection",
   "AVATAR_DEFAULT_ALT": "avatar",
@@ -68,7 +71,7 @@
   "DOWNLOAD_ITEM_BUTTON": "Télécharger",
   "DROP_DOWN_PLACEHOLDER": "Choisir dans la liste",
   "DROPZONE_HELPER_TEXT": "Déposez vos fichiers ici pour les téléverser",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Maximum 15 fichiers de 1 Go chacun",
+  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Maximum {{max}} fichiers de 1 Go chacun",
   "DROPZONE_HELPER_TUTORIALS": "Par où commencer ? Cliquer ici pour plus d'informations",
   "DROPZONE_HELPER_ACTION": "Parcourir les fichiers",
   "DROPZONE_HELPER_OPTIONAL_ACTION_TEXT": "ou",
diff --git a/src/locales/fr/messages.json b/src/locales/fr/messages.json
index 75bd3c4d4..f5e084e96 100644
--- a/src/locales/fr/messages.json
+++ b/src/locales/fr/messages.json
@@ -33,7 +33,7 @@
   "FILE_IS_NOT_IMAGE": "Le fichier n'est pas une image",
   "INVALID_UPLOAD_PARAMETERS": "Un ou plusieurs paramètres de téléchargement ne sont pas valides",
   "UPLOAD_EMPTY_FILE": "Impossible de télécharger un fichier vide",
-  "UPLOAD_TOO_MANY_FILES": "Impossible de télécharger trop de fichiers",
+  "UPLOAD_TOO_MANY_FILES": "Impossible de télécharger autant de fichiers",
   "UPLOAD_BIG_FILES": "Impossible de télécharger des fichiers volumineux",
   "INVALID_FILE_PATH_FOR_COPY": "Le chemin du fichier soumis n'est pas valide pour la copie",
   "INVALID_FILE_PATH_FOR_DELETE": "Le chemin du fichier soumis n'est pas valide pour la suppression",
@@ -41,7 +41,7 @@
   "INVALID_DOWNLOAD_PARAMETERS": "Un ou plusieurs paramètres de téléchargement ne sont pas valides",
   "LOCAL_FILE_NOT_FOUND": "Le fichier local n'est pas trouvé",
   "S3_FILE_NOT_FOUND": "Le fichier S3 n'est pas trouvé",
-  "UPLOAD_FILE_UNEXPECTED_ERROR": "Une erreur inattendue s'est produite lors du téléchargement d'un fichier",
+  "UPLOAD_FILE_UNEXPECTED_ERROR": "Une erreur inattendue s'est produite lors du téléchargement de fichiers",
   "DOWNLOAD_FILE_UNEXPECTED_ERROR": "Une erreur inattendue s'est produite lors du téléchargement d'un fichier",
   "RESTORE_ITEMS": "Vous avez restauré avec succès le(s) élément(s)",
   "CREATE_ITEM": "L'élément a été créé avec succès",
diff --git a/src/locales/it/builder.json b/src/locales/it/builder.json
index 241c12428..1b4f401d8 100644
--- a/src/locales/it/builder.json
+++ b/src/locales/it/builder.json
@@ -67,7 +67,7 @@
   "DOWNLOAD_ITEM_BUTTON": "Download",
   "DROP_DOWN_PLACEHOLDER": "Si prega di scegliere dall'elenco",
   "DROPZONE_HELPER_TEXT": "Trascina qui i tuoi file per caricarli",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Massimo 15 file da 1 GB",
+  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Massimo {{max}} file da 1 GB",
   "DROPZONE_HELPER_ACTION": "Sfoglia file",
   "DROPZONE_HELPER_OPTIONAL_ACTION_TEXT": "O",
   "DESCRIPTION_LABEL": "Descrizione",
diff --git a/src/locales/it/messages.json b/src/locales/it/messages.json
index ff441ab06..e1da13848 100644
--- a/src/locales/it/messages.json
+++ b/src/locales/it/messages.json
@@ -41,7 +41,6 @@
   "INVALID_DOWNLOAD_PARAMETERS": "Uno o più parametri di download non sono validi",
   "LOCAL_FILE_NOT_FOUND": "Il file locale non è stato trovato",
   "S3_FILE_NOT_FOUND": "Il file S3 non è stato trovato",
-  "UPLOAD_FILE_UNEXPECTED_ERROR": "Si è verificato un errore imprevisto durante il caricamento di un file",
   "DOWNLOAD_FILE_UNEXPECTED_ERROR": "Si è verificato un errore imprevisto durante il download di un file",
   "RESTORE_ITEMS": "Hai ripristinato correttamente gli elementi",
   "CREATE_ITEM": "L'articolo è stato creato con successo",
diff --git a/src/modules/builder/components/file/FileUploader.tsx b/src/modules/builder/components/file/FileUploader.tsx
index 54c60196c..4363e2ab9 100644
--- a/src/modules/builder/components/file/FileUploader.tsx
+++ b/src/modules/builder/components/file/FileUploader.tsx
@@ -11,6 +11,7 @@ import { HelpCircleIcon } from 'lucide-react';
 
 import { CustomLink } from '@/components/ui/CustomLink';
 import { NS } from '@/config/constants';
+import { getErrorMessage } from '@/config/notifier';
 import { mutations } from '@/config/queryClient';
 import { useButtonColor } from '@/ui/buttons/hooks';
 import FileDropper from '@/ui/upload/FileDropper/FileDropper';
@@ -36,6 +37,7 @@ export function FileUploader({
   previousItemId,
 }: Readonly<Props>): JSX.Element | null {
   const { t } = useTranslation(NS.Builder);
+  const { t: translateMessage } = useTranslation(NS.Messages);
   const { itemId: parentItemId } = useParams({ strict: false });
   const [error, setError] = useState<string>();
 
@@ -43,14 +45,10 @@ export function FileUploader({
   const { color } = useButtonColor('primary');
   const [totalProgress, setTotalProgress] = useState(0);
 
-  // send n request as long as the backend cannot handle multi files
-  // complex notification handling to keep one uploading toast
-  // trigger n success toast
   const onDrop = async (files: File[]): Promise<void> => {
     // update progress callback function scaled over the number of files sent
-    const updateForManyFiles = (idx: number) => (e: AxiosProgressEvent) => {
-      // suppose previous files are completely uploaded
-      const progress = ((e.progress ?? 0) + idx) / files.length;
+    const updateForManyFiles = () => (e: AxiosProgressEvent) => {
+      const progress = (e.progress ?? 0) / files.length;
       setTotalProgress(progress);
       onUpdate?.({ ...e, progress });
     };
@@ -72,25 +70,34 @@ export function FileUploader({
 
     onStart?.();
 
-    for (let idx = 0; idx < files.length; idx += 1) {
-      try {
-        await uploadFiles({
-          files: [files[idx]],
-          id: parentItemId,
-          previousItemId,
-          onUploadProgress: updateForManyFiles(idx),
-        });
-      } catch (e) {
-        onError?.(e as Error);
+    try {
+      await uploadFiles({
+        files,
+        id: parentItemId,
+        previousItemId,
+        onUploadProgress: updateForManyFiles(),
+      });
+      onComplete?.();
+    } catch (e) {
+      const message = getErrorMessage(e);
+      setError(
+        translateMessage(message) ??
+          translateMessage('UPLOAD_FILES_UNEXPECTED_ERROR'),
+      );
+
+      if (e instanceof Error) {
+        onError?.(e);
       }
+      console.error(e);
     }
-    onComplete?.();
   };
 
   const hints = (
     <Stack gap={2}>
       <Typography variant="subtitle2" textAlign="center">
-        {t('DROPZONE_HELPER_LIMIT_REMINDER_TEXT')}
+        {t('DROPZONE_HELPER_LIMIT_REMINDER_TEXT', {
+          max: MAX_NUMBER_OF_FILES_UPLOAD,
+        })}
       </Typography>
       <Stack direction="row" alignItems="center" gap={1}>
         <HelpCircleIcon size={20} color={color} />
@@ -115,7 +122,15 @@ export function FileUploader({
         uploadProgress={Math.ceil(totalProgress * 100)}
         multiple
         onDrop={onDrop}
-        error={error}
+        error={
+          error ? (
+            <>
+              {error}
+              <br />
+              {t('FILE_UPLOADER.GENERAL_ERROR')}
+            </>
+          ) : undefined
+        }
         buttonText={t('DROPZONE_HELPER_ACTION')}
         hints={hints}
         buttons={buttons}
diff --git a/src/modules/builder/components/hooks/uploadWithProgress.ts b/src/modules/builder/components/hooks/uploadWithProgress.ts
index 30dd4ef4c..7cff69d6c 100644
--- a/src/modules/builder/components/hooks/uploadWithProgress.ts
+++ b/src/modules/builder/components/hooks/uploadWithProgress.ts
@@ -5,13 +5,15 @@ import { Id, toast } from 'react-toastify';
 import { AxiosProgressEvent } from 'axios';
 
 import { NS } from '@/config/constants';
+import { getErrorMessage } from '@/config/notifier';
 
 export const useUploadWithProgress = (): {
   update: (p: AxiosProgressEvent) => void;
-  close: (e?: Error) => void;
+  close: (e?: unknown) => void;
   show: (p?: number) => void;
 } => {
   const { t: translateBuilder } = useTranslation(NS.Builder);
+  const { t: translateMessage } = useTranslation(NS.Messages);
 
   // we need to keep a reference of the toastId to be able to update it
   const toastId = useRef<Id | null>(null);
@@ -33,10 +35,14 @@ export const useUploadWithProgress = (): {
     }
   };
 
-  const close = (error?: Error) => {
+  const close = (error?: unknown) => {
     // show correct feedback message
     if (error) {
-      toast.error(error.message);
+      console.error(error);
+      toast.error(
+        translateMessage(getErrorMessage(error)) ??
+          translateMessage('UPLOAD_FILES_UNEXPECTED_ERROR'),
+      );
     } else if (toastId.current) {
       toast.done(toastId.current);
     }
diff --git a/src/modules/builder/components/item/form/file/UploadFileModalContent.tsx b/src/modules/builder/components/item/form/file/UploadFileModalContent.tsx
index fcb5e2dfc..4a6f4979a 100644
--- a/src/modules/builder/components/item/form/file/UploadFileModalContent.tsx
+++ b/src/modules/builder/components/item/form/file/UploadFileModalContent.tsx
@@ -1,4 +1,4 @@
-import type { JSX } from 'react';
+import { type JSX } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { DialogActions, DialogContent, DialogTitle } from '@mui/material';
diff --git a/src/modules/builder/components/main/ImportH5P.tsx b/src/modules/builder/components/main/ImportH5P.tsx
index 3baa968dd..fc8efb4ed 100644
--- a/src/modules/builder/components/main/ImportH5P.tsx
+++ b/src/modules/builder/components/main/ImportH5P.tsx
@@ -55,9 +55,7 @@ const ImportH5P = ({
         closeNotification();
         onClose?.();
       } catch (error) {
-        if (error instanceof Error) {
-          closeNotification(error);
-        }
+        closeNotification(error);
         console.error(error);
       }
     }
diff --git a/src/modules/builder/components/main/list/ItemsTable.tsx b/src/modules/builder/components/main/list/ItemsTable.tsx
index 35e2ab79d..f61068bf5 100644
--- a/src/modules/builder/components/main/list/ItemsTable.tsx
+++ b/src/modules/builder/components/main/list/ItemsTable.tsx
@@ -99,6 +99,7 @@ const ItemsTable = ({
           close();
         })
         .catch((e) => {
+          console.error(e);
           close(e);
         });
       return;
diff --git a/src/modules/builder/components/thumbnails/ThumbnailUploader.hook.tsx b/src/modules/builder/components/thumbnails/ThumbnailUploader.hook.tsx
index 5ee7c4197..ddc424391 100644
--- a/src/modules/builder/components/thumbnails/ThumbnailUploader.hook.tsx
+++ b/src/modules/builder/components/thumbnails/ThumbnailUploader.hook.tsx
@@ -97,7 +97,8 @@ export const useThumbnailUploader = ({
       console.error(error);
       setIsUploadingError(true);
       updateHasThumbnail(false);
-      closeNotification(error as Error);
+
+      closeNotification(error);
     } finally {
       setIsThumbnailUploading(false);
       setUploadingProgress(0);
diff --git a/src/ui/upload/FileDropper/FileDropper.stories.tsx b/src/ui/upload/FileDropper/FileDropper.stories.tsx
index 580984cf7..1e8fff2ba 100644
--- a/src/ui/upload/FileDropper/FileDropper.stories.tsx
+++ b/src/ui/upload/FileDropper/FileDropper.stories.tsx
@@ -63,9 +63,10 @@ export const Container = {
   },
 } satisfies Story;
 
+const errorText = 'You cannot upload more than 10 files at a time';
 export const WithError = {
   args: {
-    error: 'You cannot upload more than 10 files at a time',
+    error: <>{errorText}</>,
     hints: 'Max 15GB',
   },
   decorators: [
@@ -73,9 +74,9 @@ export const WithError = {
       return <Box height="400px">{story()}</Box>;
     },
   ],
-  play: ({ canvasElement, args }) => {
+  play: ({ canvasElement }) => {
     const canvas = within(canvasElement);
-    expect(canvas.getByText(args.error!).parentNode).toHaveRole('alert');
+    expect(canvas.getByText(errorText).parentNode).toHaveRole('alert');
     expect(canvas.getByRole('dropzone')).toHaveStyle({
       'background-color': '#ffbaba',
     });
diff --git a/src/ui/upload/FileDropper/FileDropper.tsx b/src/ui/upload/FileDropper/FileDropper.tsx
index 5e5186935..b5e8364e6 100644
--- a/src/ui/upload/FileDropper/FileDropper.tsx
+++ b/src/ui/upload/FileDropper/FileDropper.tsx
@@ -30,7 +30,7 @@ export type FileDropperProps = {
   /**
    * Error to show
    */
-  error?: string;
+  error?: ReactNode;
   /**
    * Smaller text to show, such as limits
    */


Reviews:
user: login: spaenleh
body: Looks good, I added some comments, le me know if you have questions.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-09T06:12:33Z
user: login: pyphilia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T12:49:02Z
user: login: spaenleh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T14:08:16Z

Review comments:
diff_hunk: @@ -6,6 +6,9 @@
     "RECYCLED_ITEMS": "Trash",
     "RETURN_HOME": "Home"
   },
+  "FILE_UPLOADER": {
+    "ERROR_WRAPPER": "Some files might have not been uploaded because of the following reason: {{error}}"
path: src/locales/en/builder.json
user: login: spaenleh
body: I think we know there are some that were not uplaoded, it is not an assumption.
```suggestion
    "ERROR_WRAPPER": "Some files were not uploaded because of the following error: {{error}}"
```
created_at: 2025-05-09T06:06:43Z
author_association: MEMBER
diff_hunk: @@ -68,7 +71,7 @@
   "DOWNLOAD_ITEM_BUTTON": "Télécharger",
   "DROP_DOWN_PLACEHOLDER": "Choisir dans la liste",
   "DROPZONE_HELPER_TEXT": "Déposez vos fichiers ici pour les téléverser",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Maximum 15 fichiers de 1 Go chacun",
+  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Maximum {{max}} fichiers de 1 Go chacun",
path: src/locales/fr/builder.json
user: login: spaenleh
body: Actually, this means you could upload 15Gb of data, but it won't work.
created_at: 2025-05-09T06:08:03Z
author_association: MEMBER
diff_hunk: @@ -67,7 +67,7 @@
   "DOWNLOAD_ITEM_BUTTON": "Download",
   "DROP_DOWN_PLACEHOLDER": "Si prega di scegliere dall'elenco",
   "DROPZONE_HELPER_TEXT": "Trascina qui i tuoi file per caricarli",
-  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Massimo 15 file da 1 GB",
+  "DROPZONE_HELPER_LIMIT_REMINDER_TEXT": "Massimo {{max}} file da 1 GB",
path: src/locales/it/builder.json
user: login: spaenleh
body: The precision that the 1GB applied to each file is lost in other languages. Maybe we should re-generate the translations.
created_at: 2025-05-09T06:09:52Z
author_association: MEMBER
diff_hunk: @@ -33,10 +35,14 @@ export const useUploadWithProgress = (): {
     }
   };
 
-  const close = (error?: Error) => {
+  const close = (error?: unknown) => {
     // show correct feedback message
     if (error) {
-      toast.error(error.message);
+      console.error(error);
path: src/modules/builder/components/hooks/uploadWithProgress.ts
user: login: spaenleh
body: Keep, or remove ?
created_at: 2025-05-09T06:11:33Z
author_association: MEMBER
diff_hunk: @@ -99,6 +99,7 @@ const ItemsTable = ({
           close();
         })
         .catch((e) => {
+          console.error(e);
path: src/modules/builder/components/main/list/ItemsTable.tsx
user: login: spaenleh
body: Keep ?
created_at: 2025-05-09T06:12:00Z
author_association: MEMBER
diff_hunk: @@ -33,10 +35,14 @@ export const useUploadWithProgress = (): {
     }
   };
 
-  const close = (error?: Error) => {
+  const close = (error?: unknown) => {
     // show correct feedback message
     if (error) {
-      toast.error(error.message);
+      console.error(error);
path: src/modules/builder/components/hooks/uploadWithProgress.ts
user: login: pyphilia
body: I like having the `console.error`, personally.
created_at: 2025-05-09T12:49:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,10 +35,14 @@ export const useUploadWithProgress = (): {
     }
   };
 
-  const close = (error?: Error) => {
+  const close = (error?: unknown) => {
     // show correct feedback message
     if (error) {
-      toast.error(error.message);
+      console.error(error);
path: src/modules/builder/components/hooks/uploadWithProgress.ts
user: login: spaenleh
body: okay no worries, maybe it would be better to use the logger. I was asking in order to know if it was intentional. May be add a comment above so it makes it explicit, or use the logger.
created_at: 2025-05-09T14:08:16Z
author_association: MEMBER

PR comments:
user: login: spaenleh
created_at: 2025-05-09T06:04:47Z
body: A comment first about the message shown in the dropzone: I think it is too lengthy. It took me too long to understand what the issue was. Should we put the "real" issue in bold and first in the message:

"**Empty file could not be uploaded**. Some files could not be uploaded, please fix the issue and try again."

Maybe stating the issue first and then giving information about the fact that maybe some still were successful ? But this thing where some will be uploaded and some not, I am not a big fan. It looks like we will have user frustration when they end-up with 5 copies of the same document just because one of them did not want to upload correctly (or did not meet the requirements).  Maybe Okay for now, but definitely should improve the UI and processing to make it more transparent what worked and what did not. 
An idea would be to see the list of files and put checkmarks and crosses in front of file names so you know which oen failed and which one is okay, so you only retry the one that failed. 
author_association: MEMBER
user: login: sonarqubecloud[bot]
created_at: 2025-05-12T10:34:15Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=graasp_graasp-account&pullRequest=995) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=graasp_graasp-account&pullRequest=995&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=graasp_graasp-account&pullRequest=995&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=graasp_graasp-account&pullRequest=995&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=graasp_graasp-account&pullRequest=995&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=graasp_graasp-account&pullRequest=995&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=graasp_graasp-account&pullRequest=995)


author_association: NONE