From 95a4349126844d973f7ea5978b014fa1b3fc7900 Mon Sep 17 00:00:00 2001 From: Syed Ali Abbas Zaidi Date: Thu, 6 Aug 2026 16:59:34 +0500 Subject: [PATCH 1/2] feat(web): allow attaching multiple files at once to a work item Work item attachments were limited to one file at a time. All three attachment dropzones passed `multiple: false` to react-dropzone and read only `acceptedFiles[0]`, so dragging several files uploaded the first and silently discarded the rest with no error shown. Upload every accepted file with bounded concurrency (3 in flight) so a large drop does not open one request per file. Per-file failures are non-fatal: the batch keeps its successes and names only the files that failed. Files rejected for exceeding the size limit are reported separately, also by name. The single-file logic was duplicated across the upload card, the quick action button and the attachment list, so it now lives in one shared `useAttachmentDropHandler` hook and all three surfaces stay in sync. No API change is needed: the existing endpoint already creates one asset and one presigned POST per call, so the client simply fans out N calls. The per-file size limit still applies unchanged. i18n: add six attachment keys across all 19 locales and remove `only_one_file_allowed` and `file_size_limit`, which no longer apply. Closes #9544 --- .../attachment/attachment-item-list.tsx | 58 ++---- .../issues/attachment/attachment-upload.tsx | 41 ++--- .../attachments/helper.tsx | 170 ++++++++++++++++-- .../attachments/quick-action-button.tsx | 65 ++----- packages/i18n/src/locales/cs/common.json | 10 +- packages/i18n/src/locales/de/common.json | 10 +- packages/i18n/src/locales/en/common.json | 10 +- packages/i18n/src/locales/es/common.json | 10 +- packages/i18n/src/locales/fr/common.json | 10 +- packages/i18n/src/locales/id/common.json | 10 +- packages/i18n/src/locales/it/common.json | 10 +- packages/i18n/src/locales/ja/common.json | 10 +- packages/i18n/src/locales/ko/common.json | 10 +- packages/i18n/src/locales/pl/common.json | 10 +- packages/i18n/src/locales/pt-BR/common.json | 10 +- packages/i18n/src/locales/ro/common.json | 10 +- packages/i18n/src/locales/ru/common.json | 10 +- packages/i18n/src/locales/sk/common.json | 10 +- packages/i18n/src/locales/tr-TR/common.json | 10 +- packages/i18n/src/locales/ua/common.json | 10 +- packages/i18n/src/locales/vi-VN/common.json | 10 +- packages/i18n/src/locales/zh-CN/common.json | 10 +- packages/i18n/src/locales/zh-TW/common.json | 10 +- 23 files changed, 329 insertions(+), 195 deletions(-) diff --git a/apps/web/core/components/issues/attachment/attachment-item-list.tsx b/apps/web/core/components/issues/attachment/attachment-item-list.tsx index 4341e5c9510..9dc9e9c41fa 100644 --- a/apps/web/core/components/issues/attachment/attachment-item-list.tsx +++ b/apps/web/core/components/issues/attachment/attachment-item-list.tsx @@ -4,13 +4,11 @@ * See the LICENSE file for details. */ -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { observer } from "mobx-react"; -import type { FileRejection } from "react-dropzone"; import { useDropzone } from "react-dropzone"; import { UploadCloud } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TIssueServiceType } from "@plane/types"; import { EIssueServiceType } from "@plane/types"; // hooks @@ -19,6 +17,7 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useFileSize } from "@/hooks/use-file-size"; // types import type { TAttachmentHelpers } from "../issue-detail-widgets/attachments/helper"; +import { useAttachmentDropHandler } from "../issue-detail-widgets/attachments/helper"; // components import { IssueAttachmentsListItem } from "./attachment-list-item"; import { IssueAttachmentsUploadItem } from "./attachment-list-upload-item"; @@ -44,8 +43,6 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList issueServiceType = EIssueServiceType.ISSUES, } = props; const { t } = useTranslation(); - // states - const [isUploading, setIsUploading] = useState(false); // store hooks const { attachment: { getAttachmentsByIssueId }, @@ -62,58 +59,27 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList const issueAttachments = getAttachmentsByIssueId(issueId); // handlers - const handleFetchPropertyActivities = useCallback(() => { + const handleUploadSettled = useCallback(() => { fetchActivities(workspaceSlug, projectId, issueId); }, [fetchActivities, workspaceSlug, projectId, issueId]); - const onDrop = useCallback( - (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { - const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length; - - if (rejectedFiles.length === 0) { - const currentFile: File = acceptedFiles[0]; - if (!currentFile || !workspaceSlug) return; - - setIsUploading(true); - createAttachment(currentFile) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("toast.error"), - message: t("attachment.error"), - }); - }) - .finally(() => { - handleFetchPropertyActivities(); - setIsUploading(false); - }); - return; - } - - setToast({ - type: TOAST_TYPE.ERROR, - title: t("toast.error"), - message: - totalAttachedFiles > 1 - ? t("attachment.only_one_file_allowed") - : t("attachment.file_size_limit", { size: maxFileSize / 1024 / 1024 }), - }); - return; - }, - [createAttachment, maxFileSize, workspaceSlug, handleFetchPropertyActivities] - ); + const { onDrop, isUploading } = useAttachmentDropHandler({ + create: createAttachment, + maxFileSize, + onUploadSettled: handleUploadSettled, + }); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, maxSize: maxFileSize, - multiple: false, - disabled: isUploading || disabled, + multiple: true, + disabled: isUploading || disabled || !workspaceSlug, }); return ( <> - {uploadStatus?.map((uploadStatus) => ( - + {uploadStatus?.map((status) => ( + ))} {issueAttachments && ( <> diff --git a/apps/web/core/components/issues/attachment/attachment-upload.tsx b/apps/web/core/components/issues/attachment/attachment-upload.tsx index 2a37b3b985c..b6b90f21814 100644 --- a/apps/web/core/components/issues/attachment/attachment-upload.tsx +++ b/apps/web/core/components/issues/attachment/attachment-upload.tsx @@ -4,13 +4,13 @@ * See the LICENSE file for details. */ -import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useDropzone } from "react-dropzone"; // plane web hooks import { useFileSize } from "@/hooks/use-file-size"; // types import type { TAttachmentOperations } from "../issue-detail-widgets/attachments/helper"; +import { useAttachmentDropHandler } from "../issue-detail-widgets/attachments/helper"; type TAttachmentOperationsModal = Pick; @@ -22,32 +22,21 @@ type Props = { export const IssueAttachmentUpload = observer(function IssueAttachmentUpload(props: Props) { const { workspaceSlug, disabled = false, attachmentOperations } = props; - // states - const [isLoading, setIsLoading] = useState(false); // file size const { maxFileSize } = useFileSize(); + // drop handler + const { onDrop, progress, isUploading } = useAttachmentDropHandler({ + create: attachmentOperations.create, + maxFileSize, + }); - const onDrop = useCallback( - (acceptedFiles: File[]) => { - const currentFile: File = acceptedFiles[0]; - if (!currentFile || !workspaceSlug) return; - - setIsLoading(true); - attachmentOperations.create(currentFile).finally(() => setIsLoading(false)); - }, - [attachmentOperations, workspaceSlug] - ); - - const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({ + const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({ onDrop, maxSize: maxFileSize, - multiple: false, - disabled: isLoading || disabled, + multiple: true, + disabled: isUploading || disabled || !workspaceSlug, }); - const fileError = - fileRejections.length > 0 ? `Invalid file type or size (max ${maxFileSize / 1024 / 1024} MB)` : null; - return (
{isDragActive ? (

Drop here...

- ) : fileError ? ( -

{fileError}

- ) : isLoading ? ( -

Uploading...

+ ) : progress ? ( +

+ {progress.total > 1 + ? `Uploading ${Math.min(progress.completed + 1, progress.total)}/${progress.total}...` + : "Uploading..."} +

) : ( -

Click or drag a file here

+

Click or drag files here

)}
diff --git a/apps/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx b/apps/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx index e32dfbbd098..5f7aa60a1f7 100644 --- a/apps/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx +++ b/apps/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx @@ -4,8 +4,10 @@ * See the LICENSE file for details. */ -import { useMemo } from "react"; -import { setPromiseToast, TOAST_TYPE, setToast } from "@plane/propel/toast"; +import { useCallback, useMemo, useState } from "react"; +import type { FileRejection } from "react-dropzone"; +import { useTranslation } from "@plane/i18n"; +import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TIssueServiceType } from "@plane/types"; import { EIssueServiceType } from "@plane/types"; // hooks @@ -13,8 +15,24 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail"; // types import type { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store"; +/** + * Number of attachments uploaded in parallel. Dropping a folder of screenshots should not + * open one request per file, but a strict sequence makes a large batch needlessly slow. + */ +const UPLOAD_CONCURRENCY = 3; + +type TAttachmentUploadSummary = { + uploadedCount: number; + failedFileNames: string[]; +}; + +export type TAttachmentUploadProgress = { + completed: number; + total: number; +}; + export type TAttachmentOperations = { - create: (file: File) => Promise; + create: (files: File[], onProgress?: (progress: TAttachmentUploadProgress) => void) => Promise; remove: (attachmentId: string) => Promise; }; @@ -27,34 +45,90 @@ export type TAttachmentHelpers = { snapshot: TAttachmentSnapshot; }; +/** + * Run `task` over every file, keeping at most `limit` uploads in flight. A rejected upload + * is recorded and never aborts the batch, so one bad file cannot discard the rest. + */ +const uploadWithConcurrency = async ( + files: File[], + limit: number, + task: (file: File) => Promise, + onSettled: (file: File, isSuccess: boolean) => void +): Promise => { + let cursor = 0; + const worker = async () => { + while (cursor < files.length) { + const file = files[cursor]; + cursor += 1; + if (!file) continue; + try { + // Sequential by design: each worker drains the queue one file at a time so that + // `limit` bounds the in-flight uploads. Promise.all here would be unbounded. + // eslint-disable-next-line no-await-in-loop + await task(file); + onSettled(file, true); + } catch { + // The store logs the underlying error; the caller turns this into a user-facing message. + onSettled(file, false); + } + } + }; + await Promise.all(Array.from({ length: Math.min(limit, files.length) }, worker)); +}; + export const useAttachmentOperations = ( workspaceSlug: string, projectId: string, issueId: string, issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES ): TAttachmentHelpers => { + const { t } = useTranslation(); const { attachment: { createAttachment, removeAttachment, getAttachmentsUploadStatusByIssueId }, } = useIssueDetail(issueServiceType); const attachmentOperations: TAttachmentOperations = useMemo( () => ({ - create: async (file) => { + create: async (files, onProgress) => { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); - const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, file); - setPromiseToast(attachmentUploadPromise, { - loading: "Uploading attachment...", - success: { - title: "Attachment uploaded", - message: () => "The attachment has been successfully uploaded", - }, - error: { - title: "Attachment not uploaded", - message: () => "The attachment could not be uploaded", - }, - }); + if (files.length === 0) return; + + const summary: TAttachmentUploadSummary = { uploadedCount: 0, failedFileNames: [] }; + let completed = 0; + + await uploadWithConcurrency( + files, + UPLOAD_CONCURRENCY, + (file) => createAttachment(workspaceSlug, projectId, issueId, file), + (file, isSuccess) => { + if (isSuccess) summary.uploadedCount += 1; + else summary.failedFileNames.push(file.name); + completed += 1; + onProgress?.({ completed, total: files.length }); + } + ); + + // A partially successful batch keeps the uploaded files and names only the ones that failed. + if (summary.failedFileNames.length > 0) { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("attachment.upload_failed_title", { count: summary.failedFileNames.length }), + message: + summary.uploadedCount > 0 + ? t("attachment.upload_partial_failure", { + count: summary.uploadedCount, + files: summary.failedFileNames.join(", "), + }) + : t("attachment.upload_failure", { files: summary.failedFileNames.join(", ") }), + }); + return; + } - await attachmentUploadPromise; + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("attachment.upload_success_title", { count: summary.uploadedCount }), + message: t("attachment.upload_success", { count: summary.uploadedCount }), + }); }, remove: async (attachmentId) => { try { @@ -74,7 +148,7 @@ export const useAttachmentOperations = ( } }, }), - [workspaceSlug, projectId, issueId, createAttachment, removeAttachment] + [workspaceSlug, projectId, issueId, createAttachment, removeAttachment, t] ); const attachmentsUploadStatus = getAttachmentsUploadStatusByIssueId(issueId); @@ -83,3 +157,63 @@ export const useAttachmentOperations = ( snapshot: { uploadStatus: attachmentsUploadStatus }, }; }; + +type TAttachmentDropHandlerArgs = { + create: TAttachmentOperations["create"]; + maxFileSize: number; + /** Runs once per drop, after the whole batch settles. */ + onUploadSettled?: () => void; +}; + +/** + * Shared `onDrop` for every work item attachment dropzone. Files rejected by the dropzone + * itself (over the size limit) are reported by name and the remaining ones still upload. + */ +export const useAttachmentDropHandler = (args: TAttachmentDropHandlerArgs) => { + const { create, maxFileSize, onUploadSettled } = args; + const { t } = useTranslation(); + // states + const [progress, setProgress] = useState(null); + + const onDrop = useCallback( + async (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + if (rejectedFiles.length > 0) { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("toast.error"), + message: t("attachment.files_too_large", { + count: rejectedFiles.length, + size: maxFileSize / 1024 / 1024, + files: rejectedFiles.map((rejection) => rejection.file.name).join(", "), + }), + }); + } + + if (acceptedFiles.length === 0) return; + + setProgress({ completed: 0, total: acceptedFiles.length }); + try { + await create(acceptedFiles, setProgress); + } catch (error) { + // Per-file failures are already reported by `create`; this only catches a batch that + // never started, such as a missing workspace or project id. + console.error("Error in uploading issue attachments:", error); + setToast({ + type: TOAST_TYPE.ERROR, + title: t("toast.error"), + message: t("attachment.error"), + }); + } finally { + setProgress(null); + onUploadSettled?.(); + } + }, + [create, maxFileSize, onUploadSettled, t] + ); + + return { + onDrop, + progress, + isUploading: progress !== null, + }; +}; diff --git a/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx b/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx index dc6155550f0..be6ea76f108 100644 --- a/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx +++ b/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx @@ -4,20 +4,18 @@ * See the LICENSE file for details. */ -import React, { useCallback, useState } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react"; -import type { FileRejection } from "react-dropzone"; import { useDropzone } from "react-dropzone"; import { PlusIcon } from "@plane/propel/icons"; // plane imports -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TIssueServiceType } from "@plane/types"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; // plane web hooks import { useFileSize } from "@/hooks/use-file-size"; // local imports -import { useAttachmentOperations } from "./helper"; +import { useAttachmentDropHandler, useAttachmentOperations } from "./helper"; type Props = { workspaceSlug: string; @@ -30,8 +28,6 @@ type Props = { export const IssueAttachmentActionButton = observer(function IssueAttachmentActionButton(props: Props) { const { workspaceSlug, projectId, issueId, customButton, disabled = false, issueServiceType } = props; - // state - const [isLoading, setIsLoading] = useState(false); // store hooks const { setLastWidgetAction, fetchActivities } = useIssueDetail(issueServiceType); // file size @@ -44,60 +40,31 @@ export const IssueAttachmentActionButton = observer(function IssueAttachmentActi issueServiceType ); // handlers - const handleFetchPropertyActivities = useCallback(() => { + const handleUploadSettled = useCallback(() => { fetchActivities(workspaceSlug, projectId, issueId); - }, [fetchActivities, workspaceSlug, projectId, issueId]); + setLastWidgetAction("attachments"); + }, [fetchActivities, workspaceSlug, projectId, issueId, setLastWidgetAction]); - const onDrop = useCallback( - (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { - const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length; - - if (rejectedFiles.length === 0) { - const currentFile: File = acceptedFiles[0]; - if (!currentFile || !workspaceSlug) return; - - setIsLoading(true); - attachmentOperations - .create(currentFile) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "File could not be attached. Try uploading again.", - }); - }) - .finally(() => { - handleFetchPropertyActivities(); - setLastWidgetAction("attachments"); - setIsLoading(false); - }); - return; - } - - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: - totalAttachedFiles > 1 - ? "Only one file can be uploaded at a time." - : `File must be of ${maxFileSize / 1024 / 1024}MB or less in size.`, - }); - return; - }, - [attachmentOperations, maxFileSize, workspaceSlug, handleFetchPropertyActivities, setLastWidgetAction] - ); + const { onDrop, isUploading } = useAttachmentDropHandler({ + create: attachmentOperations.create, + maxFileSize, + onUploadSettled: handleUploadSettled, + }); const { getRootProps, getInputProps } = useDropzone({ onDrop, maxSize: maxFileSize, - multiple: false, - disabled: isLoading || disabled, + multiple: true, + disabled: isUploading || disabled || !workspaceSlug, }); return ( + // Presentational wrapper: the button below is the real control, this only keeps the + // click from bubbling to the surrounding work item row. + // TODO: Remove extra div and move event propagation to button
{ - // TODO: Remove extra div and move event propagation to button e.stopPropagation(); }} > diff --git a/packages/i18n/src/locales/cs/common.json b/packages/i18n/src/locales/cs/common.json index c1e9372c24e..b065293507a 100644 --- a/packages/i18n/src/locales/cs/common.json +++ b/packages/i18n/src/locales/cs/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Soubor nelze připojit. Zkuste to prosím znovu.", - "only_one_file_allowed": "Je možné nahrát pouze jeden soubor najednou.", - "file_size_limit": "Soubor musí být menší než {size}MB.", "drag_and_drop": "Přetáhněte soubor kamkoli pro nahrání", - "delete": "Smazat přílohu" + "delete": "Smazat přílohu", + "files_too_large": "Přeskočeno {count, plural, one {# soubor} few {# soubory} many {# souboru} other {# souborů}} kvůli překročení limitu {size} MB: {files}", + "upload_success_title": "{count, plural, one {Příloha nahrána} few {Přílohy nahrány} many {Přílohy nahrány} other {Přílohy nahrány}}", + "upload_success": "{count, plural, one {Přidán # soubor.} few {Přidány # soubory.} many {Přidáno # souboru.} other {Přidáno # souborů.}}", + "upload_failed_title": "{count, plural, one {Příloha nenahrána} few {Přílohy nenahrány} many {Přílohy nenahrány} other {Přílohy nenahrány}}", + "upload_partial_failure": "{count, plural, one {Přidán # soubor.} few {Přidány # soubory.} many {Přidáno # souboru.} other {Přidáno # souborů.}} Nepodařilo se přidat: {files}", + "upload_failure": "Nepodařilo se přidat: {files}. Zkuste to prosím znovu." }, "label": { "select": "Vybrat štítek", diff --git a/packages/i18n/src/locales/de/common.json b/packages/i18n/src/locales/de/common.json index 1e18d4ee26e..36fd3ee1c59 100644 --- a/packages/i18n/src/locales/de/common.json +++ b/packages/i18n/src/locales/de/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Datei konnte nicht angehängt werden. Bitte versuchen Sie es erneut.", - "only_one_file_allowed": "Es kann jeweils nur eine Datei hochgeladen werden.", - "file_size_limit": "Die Datei muss kleiner als {size} MB sein.", "drag_and_drop": "Datei hierher ziehen, um sie hochzuladen", - "delete": "Anhang löschen" + "delete": "Anhang löschen", + "files_too_large": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} übersprungen – Limit von {size} MB überschritten: {files}", + "upload_success_title": "{count, plural, one {Anhang hochgeladen} other {Anhänge hochgeladen}}", + "upload_success": "{count, plural, one {# Datei wurde angehängt.} other {# Dateien wurden angehängt.}}", + "upload_failed_title": "{count, plural, one {Anhang nicht hochgeladen} other {Anhänge nicht hochgeladen}}", + "upload_partial_failure": "{count, plural, one {# Datei wurde angehängt.} other {# Dateien wurden angehängt.}} Nicht angehängt: {files}", + "upload_failure": "Nicht angehängt: {files}. Bitte erneut versuchen." }, "label": { "select": "Label auswählen", diff --git a/packages/i18n/src/locales/en/common.json b/packages/i18n/src/locales/en/common.json index a138304371e..8a0f93aeec3 100644 --- a/packages/i18n/src/locales/en/common.json +++ b/packages/i18n/src/locales/en/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "File could not be attached. Try uploading again.", - "only_one_file_allowed": "Only one file can be uploaded at a time.", - "file_size_limit": "File must be of {size}MB or less in size.", "drag_and_drop": "Drag and drop anywhere to upload", - "delete": "Delete attachment" + "delete": "Delete attachment", + "files_too_large": "Skipped {count, plural, one {# file} other {# files}} over the {size}MB limit: {files}", + "upload_success_title": "{count, plural, one {Attachment uploaded} other {Attachments uploaded}}", + "upload_success": "{count, plural, one {# file has been attached.} other {# files have been attached.}}", + "upload_failed_title": "{count, plural, one {Attachment not uploaded} other {Attachments not uploaded}}", + "upload_partial_failure": "{count, plural, one {# file was attached.} other {# files were attached.}} Could not attach: {files}", + "upload_failure": "Could not attach: {files}. Try uploading again." }, "label": { "select": "Add labels", diff --git a/packages/i18n/src/locales/es/common.json b/packages/i18n/src/locales/es/common.json index 64440c9c653..57a4d8b56bb 100644 --- a/packages/i18n/src/locales/es/common.json +++ b/packages/i18n/src/locales/es/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "No se pudo adjuntar el archivo. Intenta subirlo de nuevo.", - "only_one_file_allowed": "Solo se puede subir un archivo a la vez.", - "file_size_limit": "El archivo debe tener {size}MB o menos de tamaño.", "drag_and_drop": "Arrastra y suelta en cualquier lugar para subir", - "delete": "Eliminar archivo adjunto" + "delete": "Eliminar archivo adjunto", + "files_too_large": "Se {count, plural, one {omitió # archivo} other {omitieron # archivos}} por superar el límite de {size} MB: {files}", + "upload_success_title": "{count, plural, one {Archivo adjuntado} other {Archivos adjuntados}}", + "upload_success": "{count, plural, one {Se adjuntó # archivo.} other {Se adjuntaron # archivos.}}", + "upload_failed_title": "{count, plural, one {Archivo no adjuntado} other {Archivos no adjuntados}}", + "upload_partial_failure": "{count, plural, one {Se adjuntó # archivo.} other {Se adjuntaron # archivos.}} No se pudo adjuntar: {files}", + "upload_failure": "No se pudo adjuntar: {files}. Inténtelo de nuevo." }, "label": { "select": "Seleccionar etiqueta", diff --git a/packages/i18n/src/locales/fr/common.json b/packages/i18n/src/locales/fr/common.json index a94ab8aba2d..164860feb6f 100644 --- a/packages/i18n/src/locales/fr/common.json +++ b/packages/i18n/src/locales/fr/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Le fichier n’a pas pu être joint. Essayez de le télécharger à nouveau.", - "only_one_file_allowed": "Un seul fichier peut être téléchargé à la fois.", - "file_size_limit": "Le fichier doit faire {size}MB ou moins.", "drag_and_drop": "Glissez-déposez n’importe où pour uploader", - "delete": "Supprimer la pièce jointe" + "delete": "Supprimer la pièce jointe", + "files_too_large": "{count, plural, one {# fichier ignoré} many {# fichiers ignorés} other {# fichiers ignorés}} car au-delà de la limite de {size} Mo : {files}", + "upload_success_title": "{count, plural, one {Pièce jointe ajoutée} many {Pièces jointes ajoutées} other {Pièces jointes ajoutées}}", + "upload_success": "{count, plural, one {# fichier a été joint.} many {# fichiers ont été joints.} other {# fichiers ont été joints.}}", + "upload_failed_title": "{count, plural, one {Pièce jointe non ajoutée} many {Pièces jointes non ajoutées} other {Pièces jointes non ajoutées}}", + "upload_partial_failure": "{count, plural, one {# fichier a été joint.} many {# fichiers ont été joints.} other {# fichiers ont été joints.}} Impossible de joindre : {files}", + "upload_failure": "Impossible de joindre : {files}. Veuillez réessayer." }, "label": { "select": "Sélectionner une étiquette", diff --git a/packages/i18n/src/locales/id/common.json b/packages/i18n/src/locales/id/common.json index 7d266850dc4..37bf2404104 100644 --- a/packages/i18n/src/locales/id/common.json +++ b/packages/i18n/src/locales/id/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "File tidak dapat dilampirkan. Coba unggah lagi.", - "only_one_file_allowed": "Hanya satu file yang dapat diunggah pada satu waktu.", - "file_size_limit": "File harus berukuran {size}MB atau lebih kecil.", "drag_and_drop": "Seret dan jatuhkan di mana saja untuk mengunggah", - "delete": "Hapus lampiran" + "delete": "Hapus lampiran", + "files_too_large": "Melewati {count, plural, one {# berkas} other {# berkas}} yang melebihi batas {size} MB: {files}", + "upload_success_title": "{count, plural, one {Lampiran diunggah} other {Lampiran diunggah}}", + "upload_success": "{count, plural, one {# berkas telah dilampirkan.} other {# berkas telah dilampirkan.}}", + "upload_failed_title": "{count, plural, one {Lampiran tidak diunggah} other {Lampiran tidak diunggah}}", + "upload_partial_failure": "{count, plural, one {# berkas telah dilampirkan.} other {# berkas telah dilampirkan.}} Tidak dapat melampirkan: {files}", + "upload_failure": "Tidak dapat melampirkan: {files}. Silakan coba lagi." }, "label": { "select": "Pilih label", diff --git a/packages/i18n/src/locales/it/common.json b/packages/i18n/src/locales/it/common.json index 6fda1b04957..41e617e7a6c 100644 --- a/packages/i18n/src/locales/it/common.json +++ b/packages/i18n/src/locales/it/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Impossibile allegare il file. Riprova a caricarlo.", - "only_one_file_allowed": "È possibile caricare un solo file alla volta.", - "file_size_limit": "Il file deve essere di {size}MB o meno.", "drag_and_drop": "Trascina e rilascia ovunque per caricare", - "delete": "Elimina allegato" + "delete": "Elimina allegato", + "files_too_large": "{count, plural, one {# file ignorato} many {# file ignorati} other {# file ignorati}} perché supera il limite di {size} MB: {files}", + "upload_success_title": "{count, plural, one {Allegato caricato} many {Allegati caricati} other {Allegati caricati}}", + "upload_success": "{count, plural, one {# file è stato allegato.} many {# file sono stati allegati.} other {# file sono stati allegati.}}", + "upload_failed_title": "{count, plural, one {Allegato non caricato} many {Allegati non caricati} other {Allegati non caricati}}", + "upload_partial_failure": "{count, plural, one {# file è stato allegato.} many {# file sono stati allegati.} other {# file sono stati allegati.}} Impossibile allegare: {files}", + "upload_failure": "Impossibile allegare: {files}. Riprovare." }, "label": { "select": "Seleziona etichetta", diff --git a/packages/i18n/src/locales/ja/common.json b/packages/i18n/src/locales/ja/common.json index 4899411b615..4360279759d 100644 --- a/packages/i18n/src/locales/ja/common.json +++ b/packages/i18n/src/locales/ja/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "ファイルを添付できませんでした。もう一度アップロードしてください。", - "only_one_file_allowed": "一度にアップロードできるファイルは1つだけです。", - "file_size_limit": "ファイルサイズは{size}MB以下である必要があります。", "drag_and_drop": "どこにでもドラッグ&ドロップでアップロード", - "delete": "添付ファイルを削除" + "delete": "添付ファイルを削除", + "files_too_large": "{size}MB の上限を超えたため、{count, plural, one {# 件} other {# 件}}のファイルをスキップしました:{files}", + "upload_success_title": "{count, plural, one {添付ファイルをアップロードしました} other {添付ファイルをアップロードしました}}", + "upload_success": "{count, plural, one {# 件のファイルを添付しました。} other {# 件のファイルを添付しました。}}", + "upload_failed_title": "{count, plural, one {添付ファイルをアップロードできませんでした} other {添付ファイルをアップロードできませんでした}}", + "upload_partial_failure": "{count, plural, one {# 件のファイルを添付しました。} other {# 件のファイルを添付しました。}}添付できませんでした:{files}", + "upload_failure": "添付できませんでした:{files}。もう一度お試しください。" }, "label": { "select": "ラベルを選択", diff --git a/packages/i18n/src/locales/ko/common.json b/packages/i18n/src/locales/ko/common.json index 73eea6b1c25..0c07f98d465 100644 --- a/packages/i18n/src/locales/ko/common.json +++ b/packages/i18n/src/locales/ko/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "파일을 첨부할 수 없습니다. 다시 업로드하세요.", - "only_one_file_allowed": "한 번에 하나의 파일만 업로드할 수 있습니다.", - "file_size_limit": "파일 크기는 {size}MB 이하이어야 합니다.", "drag_and_drop": "업로드하려면 아무 곳에나 드래그 앤 드롭하세요", - "delete": "첨부 파일 삭제" + "delete": "첨부 파일 삭제", + "files_too_large": "{size}MB 제한을 초과한 파일 {count, plural, one {#개} other {#개}}를 건너뛰었습니다: {files}", + "upload_success_title": "{count, plural, one {첨부 파일 업로드 완료} other {첨부 파일 업로드 완료}}", + "upload_success": "{count, plural, one {파일 #개를 첨부했습니다.} other {파일 #개를 첨부했습니다.}}", + "upload_failed_title": "{count, plural, one {첨부 파일 업로드 실패} other {첨부 파일 업로드 실패}}", + "upload_partial_failure": "{count, plural, one {파일 #개를 첨부했습니다.} other {파일 #개를 첨부했습니다.}} 첨부하지 못한 파일: {files}", + "upload_failure": "다음 파일을 첨부하지 못했습니다: {files}. 다시 시도해 주십시오." }, "label": { "select": "레이블 선택", diff --git a/packages/i18n/src/locales/pl/common.json b/packages/i18n/src/locales/pl/common.json index 4c4850cbf66..b4f9607014e 100644 --- a/packages/i18n/src/locales/pl/common.json +++ b/packages/i18n/src/locales/pl/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Nie udało się dodać pliku. Spróbuj ponownie.", - "only_one_file_allowed": "Możesz przesłać tylko jeden plik naraz.", - "file_size_limit": "Plik musi być mniejszy niż {size}MB.", "drag_and_drop": "Przeciągnij plik w dowolne miejsce, aby przesłać", - "delete": "Usuń załącznik" + "delete": "Usuń załącznik", + "files_too_large": "Pominięto {count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}} z powodu przekroczenia limitu {size} MB: {files}", + "upload_success_title": "{count, plural, one {Załącznik przesłany} few {Załączniki przesłane} many {Załączniki przesłane} other {Załączniki przesłane}}", + "upload_success": "Dodano {count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}.", + "upload_failed_title": "{count, plural, one {Załącznik nieprzesłany} few {Załączniki nieprzesłane} many {Załączniki nieprzesłane} other {Załączniki nieprzesłane}}", + "upload_partial_failure": "Dodano {count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}. Nie udało się dodać: {files}", + "upload_failure": "Nie udało się dodać: {files}. Proszę spróbować ponownie." }, "label": { "select": "Wybierz etykietę", diff --git a/packages/i18n/src/locales/pt-BR/common.json b/packages/i18n/src/locales/pt-BR/common.json index 565e85d8739..7066f713542 100644 --- a/packages/i18n/src/locales/pt-BR/common.json +++ b/packages/i18n/src/locales/pt-BR/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Não foi possível anexar o arquivo. Tente enviar novamente.", - "only_one_file_allowed": "Apenas um arquivo pode ser enviado por vez.", - "file_size_limit": "O arquivo deve ter {size}MB ou menos.", "drag_and_drop": "Arraste e solte em qualquer lugar para enviar", - "delete": "Excluir anexo" + "delete": "Excluir anexo", + "files_too_large": "{count, plural, one {# arquivo ignorado} many {# arquivos ignorados} other {# arquivos ignorados}} por exceder o limite de {size} MB: {files}", + "upload_success_title": "{count, plural, one {Anexo enviado} many {Anexos enviados} other {Anexos enviados}}", + "upload_success": "{count, plural, one {# arquivo foi anexado.} many {# arquivos foram anexados.} other {# arquivos foram anexados.}}", + "upload_failed_title": "{count, plural, one {Anexo não enviado} many {Anexos não enviados} other {Anexos não enviados}}", + "upload_partial_failure": "{count, plural, one {# arquivo foi anexado.} many {# arquivos foram anexados.} other {# arquivos foram anexados.}} Não foi possível anexar: {files}", + "upload_failure": "Não foi possível anexar: {files}. Tente enviar novamente." }, "label": { "select": "Selecionar etiqueta", diff --git a/packages/i18n/src/locales/ro/common.json b/packages/i18n/src/locales/ro/common.json index e2b44ce87ec..812b7e11646 100644 --- a/packages/i18n/src/locales/ro/common.json +++ b/packages/i18n/src/locales/ro/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Fișierul nu a putut fi atașat. Încearcă să încarci din nou.", - "only_one_file_allowed": "Se poate încărca doar un fișier o dată.", - "file_size_limit": "Fișierul trebuie să aibă {size}MB sau mai puțin.", "drag_and_drop": "Trage și plasează oriunde pentru a încărca", - "delete": "Șterge atașamentul" + "delete": "Șterge atașamentul", + "files_too_large": "{count, plural, one {# fișier a fost omis} few {# fișiere au fost omise} other {# de fișiere au fost omise}} pentru depășirea limitei de {size} MB: {files}", + "upload_success_title": "{count, plural, one {Atașament încărcat} few {Atașamente încărcate} other {Atașamente încărcate}}", + "upload_success": "{count, plural, one {# fișier a fost atașat.} few {# fișiere au fost atașate.} other {# de fișiere au fost atașate.}}", + "upload_failed_title": "{count, plural, one {Atașament neîncărcat} few {Atașamente neîncărcate} other {Atașamente neîncărcate}}", + "upload_partial_failure": "{count, plural, one {# fișier a fost atașat.} few {# fișiere au fost atașate.} other {# de fișiere au fost atașate.}} Nu s-au putut atașa: {files}", + "upload_failure": "Nu s-au putut atașa: {files}. Încercați din nou." }, "label": { "select": "Selectează eticheta", diff --git a/packages/i18n/src/locales/ru/common.json b/packages/i18n/src/locales/ru/common.json index c2881e55f8a..3995942fe95 100644 --- a/packages/i18n/src/locales/ru/common.json +++ b/packages/i18n/src/locales/ru/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Ошибка прикрепления файла", - "only_one_file_allowed": "Можно загрузить только один файл", - "file_size_limit": "Максимальный размер файла - {size} МБ", "drag_and_drop": "Перетащите файл для загрузки", - "delete": "Удалить вложение" + "delete": "Удалить вложение", + "files_too_large": "{count, plural, one {Пропущен # файл} few {Пропущено # файла} many {Пропущено # файлов} other {Пропущено # файла}} из-за превышения лимита {size} МБ: {files}", + "upload_success_title": "{count, plural, one {Вложение загружено} few {Вложения загружены} many {Вложения загружены} other {Вложения загружены}}", + "upload_success": "{count, plural, one {Прикреплён # файл.} few {Прикреплено # файла.} many {Прикреплено # файлов.} other {Прикреплено # файла.}}", + "upload_failed_title": "{count, plural, one {Вложение не загружено} few {Вложения не загружены} many {Вложения не загружены} other {Вложения не загружены}}", + "upload_partial_failure": "{count, plural, one {Прикреплён # файл.} few {Прикреплено # файла.} many {Прикреплено # файлов.} other {Прикреплено # файла.}} Не удалось прикрепить: {files}", + "upload_failure": "Не удалось прикрепить: {files}. Попробуйте ещё раз." }, "label": { "select": "Выбрать метку", diff --git a/packages/i18n/src/locales/sk/common.json b/packages/i18n/src/locales/sk/common.json index 4fc339cece6..057047d310c 100644 --- a/packages/i18n/src/locales/sk/common.json +++ b/packages/i18n/src/locales/sk/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Súbor sa nedá pripojiť. Skúste to prosím znova.", - "only_one_file_allowed": "Je možné nahrať iba jeden súbor naraz.", - "file_size_limit": "Súbor musí byť menší ako {size}MB.", "drag_and_drop": "Pretiahnite súbor kamkoľvek pre nahratie", - "delete": "Zmazať prílohu" + "delete": "Zmazať prílohu", + "files_too_large": "Preskočené {count, plural, one {# súbor} few {# súbory} many {# súboru} other {# súborov}} nad limitom {size} MB: {files}", + "upload_success_title": "{count, plural, one {Príloha nahraná} few {Prílohy nahrané} many {Prílohy nahrané} other {Prílohy nahrané}}", + "upload_success": "{count, plural, one {Pridaný # súbor.} few {Pridané # súbory.} many {Pridaných # súboru.} other {Pridaných # súborov.}}", + "upload_failed_title": "{count, plural, one {Príloha nenahraná} few {Prílohy nenahrané} many {Prílohy nenahrané} other {Prílohy nenahrané}}", + "upload_partial_failure": "{count, plural, one {Pridaný # súbor.} few {Pridané # súbory.} many {Pridaných # súboru.} other {Pridaných # súborov.}} Nepodarilo sa pridať: {files}", + "upload_failure": "Nepodarilo sa pridať: {files}. Skúste to znova." }, "label": { "select": "Vybrať štítok", diff --git a/packages/i18n/src/locales/tr-TR/common.json b/packages/i18n/src/locales/tr-TR/common.json index 74bd5cbf9b3..0582bd09873 100644 --- a/packages/i18n/src/locales/tr-TR/common.json +++ b/packages/i18n/src/locales/tr-TR/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Dosya eklenemedi. Tekrar yüklemeyi deneyin.", - "only_one_file_allowed": "Aynı anda yalnızca bir dosya yüklenebilir.", - "file_size_limit": "Dosya boyutu {size}MB veya daha az olmalıdır.", "drag_and_drop": "Yüklemek için herhangi bir yere sürükleyip bırakın", - "delete": "Eki sil" + "delete": "Eki sil", + "files_too_large": "{size} MB sınırını aşan {count, plural, one {# dosya} other {# dosya}} atlandı: {files}", + "upload_success_title": "{count, plural, one {Ek yüklendi} other {Ekler yüklendi}}", + "upload_success": "{count, plural, one {# dosya eklendi.} other {# dosya eklendi.}}", + "upload_failed_title": "{count, plural, one {Ek yüklenmedi} other {Ekler yüklenmedi}}", + "upload_partial_failure": "{count, plural, one {# dosya eklendi.} other {# dosya eklendi.}} Eklenemedi: {files}", + "upload_failure": "Eklenemedi: {files}. Lütfen tekrar deneyin." }, "label": { "select": "Etiket seç", diff --git a/packages/i18n/src/locales/ua/common.json b/packages/i18n/src/locales/ua/common.json index 89bd906d8fa..4b1807a85bc 100644 --- a/packages/i18n/src/locales/ua/common.json +++ b/packages/i18n/src/locales/ua/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Не вдалося додати файл. Спробуйте ще раз.", - "only_one_file_allowed": "Можна завантажити лише один файл одночасно.", - "file_size_limit": "Файл має бути меншим за {size}МБ.", "drag_and_drop": "Перетягніть файл сюди для завантаження", - "delete": "Видалити вкладення" + "delete": "Видалити вкладення", + "files_too_large": "{count, plural, one {Пропущено # файл} few {Пропущено # файла} many {Пропущено # файлів} other {Пропущено # файла}} через перевищення ліміту {size} МБ: {files}", + "upload_success_title": "{count, plural, one {Вкладення завантажено} few {Вкладення завантажено} many {Вкладення завантажено} other {Вкладення завантажено}}", + "upload_success": "{count, plural, one {Прикріплено # файл.} few {Прикріплено # файла.} many {Прикріплено # файлів.} other {Прикріплено # файла.}}", + "upload_failed_title": "{count, plural, one {Вкладення не завантажено} few {Вкладення не завантажено} many {Вкладення не завантажено} other {Вкладення не завантажено}}", + "upload_partial_failure": "{count, plural, one {Прикріплено # файл.} few {Прикріплено # файла.} many {Прикріплено # файлів.} other {Прикріплено # файла.}} Не вдалося прикріпити: {files}", + "upload_failure": "Не вдалося прикріпити: {files}. Спробуйте ще раз." }, "label": { "select": "Вибрати мітку", diff --git a/packages/i18n/src/locales/vi-VN/common.json b/packages/i18n/src/locales/vi-VN/common.json index f9aae43c1b9..90ffa8a7529 100644 --- a/packages/i18n/src/locales/vi-VN/common.json +++ b/packages/i18n/src/locales/vi-VN/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "Không thể đính kèm tệp. Vui lòng tải lên lại.", - "only_one_file_allowed": "Chỉ có thể tải lên một tệp mỗi lần.", - "file_size_limit": "Kích thước tệp phải nhỏ hơn hoặc bằng {size}MB.", "drag_and_drop": "Kéo và thả vào bất kỳ đâu để tải lên", - "delete": "Xóa tệp đính kèm" + "delete": "Xóa tệp đính kèm", + "files_too_large": "Đã bỏ qua {count, plural, one {# tệp} other {# tệp}} vượt quá giới hạn {size} MB: {files}", + "upload_success_title": "{count, plural, one {Đã tải lên tệp đính kèm} other {Đã tải lên tệp đính kèm}}", + "upload_success": "{count, plural, one {Đã đính kèm # tệp.} other {Đã đính kèm # tệp.}}", + "upload_failed_title": "{count, plural, one {Chưa tải lên tệp đính kèm} other {Chưa tải lên tệp đính kèm}}", + "upload_partial_failure": "{count, plural, one {Đã đính kèm # tệp.} other {Đã đính kèm # tệp.}} Không thể đính kèm: {files}", + "upload_failure": "Không thể đính kèm: {files}. Vui lòng thử lại." }, "label": { "select": "Chọn nhãn", diff --git a/packages/i18n/src/locales/zh-CN/common.json b/packages/i18n/src/locales/zh-CN/common.json index dd67d925a07..8863407559b 100644 --- a/packages/i18n/src/locales/zh-CN/common.json +++ b/packages/i18n/src/locales/zh-CN/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "无法附加文件。请重新上传。", - "only_one_file_allowed": "一次只能上传一个文件。", - "file_size_limit": "文件大小必须小于或等于 {size}MB。", "drag_and_drop": "拖放到任意位置以上传", - "delete": "删除附件" + "delete": "删除附件", + "files_too_large": "已跳过超出 {size}MB 限制的 {count, plural, one {# 个文件} other {# 个文件}}:{files}", + "upload_success_title": "{count, plural, one {附件已上传} other {附件已上传}}", + "upload_success": "{count, plural, one {已附加 # 个文件。} other {已附加 # 个文件。}}", + "upload_failed_title": "{count, plural, one {附件未上传} other {附件未上传}}", + "upload_partial_failure": "{count, plural, one {已附加 # 个文件。} other {已附加 # 个文件。}}无法附加:{files}", + "upload_failure": "无法附加:{files}。请重试。" }, "label": { "select": "选择标签", diff --git a/packages/i18n/src/locales/zh-TW/common.json b/packages/i18n/src/locales/zh-TW/common.json index 834a2d922d8..9f078a9da59 100644 --- a/packages/i18n/src/locales/zh-TW/common.json +++ b/packages/i18n/src/locales/zh-TW/common.json @@ -759,10 +759,14 @@ }, "attachment": { "error": "無法附加檔案。請重新上傳。", - "only_one_file_allowed": "一次只能上傳一個檔案。", - "file_size_limit": "檔案大小必須小於或等於 {size}MB。", "drag_and_drop": "拖曳到任何位置以上傳", - "delete": "刪除附件" + "delete": "刪除附件", + "files_too_large": "已略過超出 {size}MB 限制的 {count, plural, one {# 個檔案} other {# 個檔案}}:{files}", + "upload_success_title": "{count, plural, one {附件已上傳} other {附件已上傳}}", + "upload_success": "{count, plural, one {已附加 # 個檔案。} other {已附加 # 個檔案。}}", + "upload_failed_title": "{count, plural, one {附件未上傳} other {附件未上傳}}", + "upload_partial_failure": "{count, plural, one {已附加 # 個檔案。} other {已附加 # 個檔案。}}無法附加:{files}", + "upload_failure": "無法附加:{files}。請重試。" }, "label": { "select": "選擇標籤", From 0f8b704a9b905c97479bbbfaa6f6096c20186d7c Mon Sep 17 00:00:00 2001 From: Syed Ali Abbas Zaidi Date: Thu, 6 Aug 2026 17:18:03 +0500 Subject: [PATCH 2/2] fix(web): correct attachment upload progress count and button disabled state Address review feedback on #9560. The progress label added one to the settled count, so a three file batch read "3/3" once only two files had finished and the Math.min clamp then repeated "3/3" for the final step. Render the completed count directly: uploads run concurrently, so there is no single "current" file to point at and completed-of-total is the only accurate reading. react-dropzone does not forward its disabled state to the root element, so the quick action button stayed natively enabled while an upload was in flight or the workspace slug was missing, leaving it clickable but inert. Give the button the same condition as the dropzone. --- .../components/issues/attachment/attachment-upload.tsx | 6 +++--- .../attachments/quick-action-button.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/web/core/components/issues/attachment/attachment-upload.tsx b/apps/web/core/components/issues/attachment/attachment-upload.tsx index b6b90f21814..281dcda8f5d 100644 --- a/apps/web/core/components/issues/attachment/attachment-upload.tsx +++ b/apps/web/core/components/issues/attachment/attachment-upload.tsx @@ -50,9 +50,9 @@ export const IssueAttachmentUpload = observer(function IssueAttachmentUpload(pro

Drop here...

) : progress ? (

- {progress.total > 1 - ? `Uploading ${Math.min(progress.completed + 1, progress.total)}/${progress.total}...` - : "Uploading..."} + {/* Files finished, not the file being worked on: uploads run concurrently, so + there is no single "current" file to point at. */} + {progress.total > 1 ? `Uploading ${progress.completed}/${progress.total}...` : "Uploading..."}

) : (

Click or drag files here

diff --git a/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx b/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx index be6ea76f108..e27ad30d273 100644 --- a/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx +++ b/apps/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx @@ -51,11 +51,15 @@ export const IssueAttachmentActionButton = observer(function IssueAttachmentActi onUploadSettled: handleUploadSettled, }); + // react-dropzone does not forward its disabled state to the root element, so the native + // button has to be given the same condition or it stays clickable while doing nothing. + const isDropzoneDisabled = isUploading || disabled || !workspaceSlug; + const { getRootProps, getInputProps } = useDropzone({ onDrop, maxSize: maxFileSize, multiple: true, - disabled: isUploading || disabled || !workspaceSlug, + disabled: isDropzoneDisabled, }); return ( @@ -68,7 +72,7 @@ export const IssueAttachmentActionButton = observer(function IssueAttachmentActi e.stopPropagation(); }} > -