feat: support multiple file upload in Attachments component (#13283)

Closes #13277

## What I Did

- Enabled `multiple` attribute in the file input
- Updated the `handleFileChange` handler to loop through files
- Confirmed that each file is sent via `uploadAttachmentFile`

## Why

This change allows users to upload multiple files at once instead of
doing it one by one.

## Screenshot / Video (Optional)
[Screencast From 2025-07-18
23-58-36.webm](https://github.com/user-attachments/assets/ea191f25-1904-4643-afe2-7029785eebcb)


---
Let me know if you'd like any changes! 💪

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
vishwas babar
2025-07-22 17:19:47 +05:30
committed by GitHub
parent d59604c92d
commit 0b5bdf1c93
4 changed files with 31 additions and 15 deletions

View File

@ -137,6 +137,12 @@ export const AttachmentList = ({
await uploadAttachmentFile(file, targetableObject);
};
const onUploadFiles = async (files: File[]) => {
for (const file of files) {
await onUploadFile(file);
}
};
const handlePreview = (attachment: Attachment) => {
if (!isAttachmentPreviewEnabled) return;
setPreviewedAttachment(attachment);
@ -167,7 +173,7 @@ export const AttachmentList = ({
{isDraggingFile ? (
<DropZone
setIsDraggingFile={setIsDraggingFile}
onUploadFile={onUploadFile}
onUploadFiles={onUploadFiles}
/>
) : (
<ActivityList>

View File

@ -48,18 +48,26 @@ export const Attachments = ({
const [isDraggingFile, setIsDraggingFile] = useState(false);
const onUploadFile = async (file: File) => {
await uploadAttachmentFile(file, targetableObject);
};
const onUploadFiles = async (files: File[]) => {
for (const file of files) {
await onUploadFile(file);
}
};
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (isDefined(e.target.files)) onUploadFile?.(e.target.files[0]);
if (isDefined(e.target.files)) {
onUploadFiles(Array.from(e.target.files));
}
};
const handleUploadFileClick = () => {
inputFileRef?.current?.click?.();
};
const onUploadFile = async (file: File) => {
await uploadAttachmentFile(file, targetableObject);
};
const isAttachmentsEmpty = !attachments || attachments.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
@ -82,7 +90,7 @@ export const Attachments = ({
{isDraggingFile ? (
<DropZone
setIsDraggingFile={setIsDraggingFile}
onUploadFile={onUploadFile}
onUploadFiles={onUploadFiles}
/>
) : (
<AnimatedPlaceholderEmptyContainer
@ -102,6 +110,7 @@ export const Attachments = ({
ref={inputFileRef}
onChange={handleFileChange}
type="file"
multiple
/>
{hasObjectUpdatePermissions && (
<Button
@ -123,6 +132,7 @@ export const Attachments = ({
ref={inputFileRef}
onChange={handleFileChange}
type="file"
multiple
/>
<AttachmentList
targetableObject={targetableObject}

View File

@ -1,6 +1,6 @@
import { useDropzone } from 'react-dropzone';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useDropzone } from 'react-dropzone';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { IconUpload } from 'twenty-ui/display';
@ -40,12 +40,12 @@ const StyledUploadIcon = styled(IconUpload)`
type DropZoneProps = {
setIsDraggingFile: (drag: boolean) => void;
onUploadFile: (file: File) => void;
onUploadFiles: (files: File[]) => void;
};
export const DropZone = ({
setIsDraggingFile,
onUploadFile,
onUploadFiles,
}: DropZoneProps) => {
const theme = useTheme();
const { maxFileSize } = useSpreadsheetImportInternal();
@ -53,7 +53,7 @@ export const DropZone = ({
const { getRootProps, getInputProps, isDragActive } = useDropzone({
noClick: true,
noKeyboard: true,
maxFiles: 1,
multiple: true,
maxSize: maxFileSize,
onDragEnter: () => {
setIsDraggingFile(true);
@ -64,8 +64,8 @@ export const DropZone = ({
onDrop: () => {
setIsDraggingFile(false);
},
onDropAccepted: async ([file]) => {
onUploadFile(file);
onDropAccepted: async (files) => {
onUploadFiles(files);
setIsDraggingFile(false);
},
});
@ -85,7 +85,7 @@ export const DropZone = ({
stroke={theme.icon.stroke.sm}
size={theme.icon.size.lg}
/>
<StyledUploadDragTitle>Upload a file</StyledUploadDragTitle>
<StyledUploadDragTitle>Upload files</StyledUploadDragTitle>
<StyledUploadDragSubTitle>
Drag and Drop Here
</StyledUploadDragSubTitle>

View File

@ -85,7 +85,7 @@ export const AIChatTab = ({
{isDraggingFile && (
<DropZone
setIsDraggingFile={setIsDraggingFile}
onUploadFile={(files) => uploadFiles([files])}
onUploadFiles={uploadFiles}
/>
)}
{!isDraggingFile && (