fix(dashboard): Limit file uploads to 1MB (#13981)

## Summary

**What** — What changes are introduced in this PR?

Limit file uploads to 1MB

**Why** — Why are these changes relevant or necessary?  

Prevent large file uploads in the Admin 

**How** — How have these changes been implemented?

Set size limits on the file uploads

**Testing** — How have these changes been tested, or how can the reviewer test the feature?

---

## Checklist

Please ensure the following before requesting a review:

- [ ] I have added a **changeset** for this PR
    - Every non-breaking change should be marked as a **patch**
    - To add a changeset, run `yarn changeset` and follow the prompts
- [ ] The changes are covered by relevant **tests**
- [ ] I have verified the code works as intended locally
- [ ] I have linked the related issue(s) if applicable

---

## Additional Context

CLOSES CORE-1270


---

> [!NOTE]
> Adds a 1MB default file size limit to `FileUpload`, surfaces size/type rejections in media forms via new i18n, and allows unlimited size for product import.
> 
> - **Components**:
>   - `components/common/file-upload/file-upload.tsx`:
>     - Add `maxFileSize` (default 1MB) and size validation; return `rejectedFiles` alongside valid files.
> - **Products › Media Upload**:
>   - `upload-media-form-item.tsx`:
>     - Handle `rejectedFiles` and set form errors for invalid type and oversized files.
> - **Products › Import**:
>   - `upload-import.tsx`: Use `maxFileSize={Infinity}` to disable size limit for CSV import.
> - **i18n**:
>   - Schema and EN translations: add `products.media.fileTooLarge` message.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c8c67f4d329f8767e99694649bf0b3fe4cf400e9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
This commit is contained in:
juanzgc
2025-11-06 09:57:24 -05:00
committed by GitHub
parent 3ca1e1df33
commit e42e6f0daa
5 changed files with 53 additions and 13 deletions

View File

@@ -8,21 +8,30 @@ export interface FileType {
file: File file: File
} }
export interface RejectedFile {
file: File
reason: "size" | "format"
}
export interface FileUploadProps { export interface FileUploadProps {
label: string label: string
multiple?: boolean multiple?: boolean
hint?: string hint?: string
hasError?: boolean hasError?: boolean
formats: string[] formats: string[]
onUploaded: (files: FileType[]) => void maxFileSize?: number // in bytes, defaults to 1MB. Set to Infinity to disable.
onUploaded: (files: FileType[], rejectedFiles?: RejectedFile[]) => void
} }
const DEFAULT_MAX_FILE_SIZE = 1024 * 1024 // 1MB
export const FileUpload = ({ export const FileUpload = ({
label, label,
hint, hint,
multiple = true, multiple = true,
hasError, hasError,
formats, formats,
maxFileSize = DEFAULT_MAX_FILE_SIZE,
onUploaded, onUploaded,
}: FileUploadProps) => { }: FileUploadProps) => {
const [isDragOver, setIsDragOver] = useState<boolean>(false) const [isDragOver, setIsDragOver] = useState<boolean>(false)
@@ -65,18 +74,26 @@ export const FileUpload = ({
} }
const fileList = Array.from(files) const fileList = Array.from(files)
const fileObj = fileList.map((file) => { const validFiles: FileType[] = []
const id = Math.random().toString(36).substring(7) const rejectedFiles: RejectedFile[] = []
const normalizedMaxFileSize = Math.min(maxFileSize, Infinity)
fileList.forEach((file) => {
if (file.size > normalizedMaxFileSize) {
rejectedFiles.push({ file, reason: "size" })
return
}
const id = Math.random().toString(36).substring(7)
const previewUrl = URL.createObjectURL(file) const previewUrl = URL.createObjectURL(file)
return { validFiles.push({
id: id, id: id,
url: previewUrl, url: previewUrl,
file, file,
} })
}) })
onUploaded(fileObj) onUploaded(validFiles, rejectedFiles)
} }
const handleDrop = (event: DragEvent) => { const handleDrop = (event: DragEvent) => {

View File

@@ -1935,6 +1935,9 @@
"invalidFileType": { "invalidFileType": {
"type": "string" "type": "string"
}, },
"fileTooLarge": {
"type": "string"
},
"failedToUpload": { "failedToUpload": {
"type": "string" "type": "string"
}, },

View File

@@ -513,6 +513,7 @@
"uploadImagesLabel": "Upload images", "uploadImagesLabel": "Upload images",
"uploadImagesHint": "Drag and drop images here or click to upload.", "uploadImagesHint": "Drag and drop images here or click to upload.",
"invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.", "invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.",
"fileTooLarge": "'{{name}}' exceeds the maximum file size of {{size}}. Please upload a smaller file.",
"failedToUpload": "Failed to upload the added media. Please try again.", "failedToUpload": "Failed to upload the added media. Please try again.",
"deleteWarning_one": "You are about to delete {{count}} image. This action cannot be undone.", "deleteWarning_one": "You are about to delete {{count}} image. This action cannot be undone.",
"deleteWarning_other": "You are about to delete {{count}} images. This action cannot be undone.", "deleteWarning_other": "You are about to delete {{count}} images. This action cannot be undone.",

View File

@@ -5,6 +5,7 @@ import { z } from "zod"
import { import {
FileType, FileType,
FileUpload, FileUpload,
RejectedFile,
} from "../../../../../components/common/file-upload" } from "../../../../../components/common/file-upload"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { MediaSchema } from "../../../product-create/constants" import { MediaSchema } from "../../../product-create/constants"
@@ -47,11 +48,13 @@ export const UploadMediaFormItem = ({
const { t } = useTranslation() const { t } = useTranslation()
const hasInvalidFiles = useCallback( const hasInvalidFiles = useCallback(
(fileList: FileType[]) => { (fileList: FileType[] = [], rejectedFiles: RejectedFile[] = []) => {
const invalidFile = fileList.find( const invalidFile = fileList.find(
(f) => !SUPPORTED_FORMATS.includes(f.file.type) (f) => !SUPPORTED_FORMATS.includes(f?.file?.type)
) )
let hasInvalidFile = false;
if (invalidFile) { if (invalidFile) {
form.setError("media", { form.setError("media", {
type: "invalid_file", type: "invalid_file",
@@ -61,22 +64,37 @@ export const UploadMediaFormItem = ({
}), }),
}) })
return true hasInvalidFile = true;
} }
return false const fileSizeRejections = rejectedFiles.filter((f) => f?.reason === "size")
if (fileSizeRejections.length) {
const fileNames = fileSizeRejections.map((f) => f.file.name).join(", ")
form.setError("media", {
type: "file_too_large",
message: t("products.media.fileTooLarge", {
name: fileNames,
size: "1MB",
}),
})
hasInvalidFile = true;
}
return hasInvalidFile;
}, },
[form, t] [form, t]
) )
const onUploaded = useCallback( const onUploaded = useCallback(
(files: FileType[]) => { (files: FileType[] = [], rejectedFiles: RejectedFile[] = []) => {
form.clearErrors("media") form.clearErrors("media")
if (hasInvalidFiles(files)) { if (hasInvalidFiles(files, rejectedFiles)) {
return return
} }
files.forEach((f) => append({ ...f, isThumbnail: false })) files?.forEach((f) => append({ ...f, isThumbnail: false }))
}, },
[form, append, hasInvalidFiles] [form, append, hasInvalidFiles]
) )

View File

@@ -48,6 +48,7 @@ export const UploadImport = ({
} }
onUploaded(files[0].file) onUploaded(files[0].file)
}} }}
maxFileSize={Infinity}
/> />
{error && ( {error && (