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:
@@ -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) => {
|
||||||
|
|||||||
@@ -1935,6 +1935,9 @@
|
|||||||
"invalidFileType": {
|
"invalidFileType": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"fileTooLarge": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"failedToUpload": {
|
"failedToUpload": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const UploadImport = ({
|
|||||||
}
|
}
|
||||||
onUploaded(files[0].file)
|
onUploaded(files[0].file)
|
||||||
}}
|
}}
|
||||||
|
maxFileSize={Infinity}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
Reference in New Issue
Block a user