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:
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -85,7 +85,7 @@ export const AIChatTab = ({
|
||||
{isDraggingFile && (
|
||||
<DropZone
|
||||
setIsDraggingFile={setIsDraggingFile}
|
||||
onUploadFile={(files) => uploadFiles([files])}
|
||||
onUploadFiles={uploadFiles}
|
||||
/>
|
||||
)}
|
||||
{!isDraggingFile && (
|
||||
|
||||
Reference in New Issue
Block a user