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);
|
await uploadAttachmentFile(file, targetableObject);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUploadFiles = async (files: File[]) => {
|
||||||
|
for (const file of files) {
|
||||||
|
await onUploadFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handlePreview = (attachment: Attachment) => {
|
const handlePreview = (attachment: Attachment) => {
|
||||||
if (!isAttachmentPreviewEnabled) return;
|
if (!isAttachmentPreviewEnabled) return;
|
||||||
setPreviewedAttachment(attachment);
|
setPreviewedAttachment(attachment);
|
||||||
@ -167,7 +173,7 @@ export const AttachmentList = ({
|
|||||||
{isDraggingFile ? (
|
{isDraggingFile ? (
|
||||||
<DropZone
|
<DropZone
|
||||||
setIsDraggingFile={setIsDraggingFile}
|
setIsDraggingFile={setIsDraggingFile}
|
||||||
onUploadFile={onUploadFile}
|
onUploadFiles={onUploadFiles}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ActivityList>
|
<ActivityList>
|
||||||
|
|||||||
@ -48,18 +48,26 @@ export const Attachments = ({
|
|||||||
|
|
||||||
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
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>) => {
|
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 = () => {
|
const handleUploadFileClick = () => {
|
||||||
inputFileRef?.current?.click?.();
|
inputFileRef?.current?.click?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUploadFile = async (file: File) => {
|
|
||||||
await uploadAttachmentFile(file, targetableObject);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAttachmentsEmpty = !attachments || attachments.length === 0;
|
const isAttachmentsEmpty = !attachments || attachments.length === 0;
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
@ -82,7 +90,7 @@ export const Attachments = ({
|
|||||||
{isDraggingFile ? (
|
{isDraggingFile ? (
|
||||||
<DropZone
|
<DropZone
|
||||||
setIsDraggingFile={setIsDraggingFile}
|
setIsDraggingFile={setIsDraggingFile}
|
||||||
onUploadFile={onUploadFile}
|
onUploadFiles={onUploadFiles}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AnimatedPlaceholderEmptyContainer
|
<AnimatedPlaceholderEmptyContainer
|
||||||
@ -102,6 +110,7 @@ export const Attachments = ({
|
|||||||
ref={inputFileRef}
|
ref={inputFileRef}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
/>
|
/>
|
||||||
{hasObjectUpdatePermissions && (
|
{hasObjectUpdatePermissions && (
|
||||||
<Button
|
<Button
|
||||||
@ -123,6 +132,7 @@ export const Attachments = ({
|
|||||||
ref={inputFileRef}
|
ref={inputFileRef}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
/>
|
/>
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
targetableObject={targetableObject}
|
targetableObject={targetableObject}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useDropzone } from 'react-dropzone';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
import { IconUpload } from 'twenty-ui/display';
|
import { IconUpload } from 'twenty-ui/display';
|
||||||
@ -40,12 +40,12 @@ const StyledUploadIcon = styled(IconUpload)`
|
|||||||
|
|
||||||
type DropZoneProps = {
|
type DropZoneProps = {
|
||||||
setIsDraggingFile: (drag: boolean) => void;
|
setIsDraggingFile: (drag: boolean) => void;
|
||||||
onUploadFile: (file: File) => void;
|
onUploadFiles: (files: File[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropZone = ({
|
export const DropZone = ({
|
||||||
setIsDraggingFile,
|
setIsDraggingFile,
|
||||||
onUploadFile,
|
onUploadFiles,
|
||||||
}: DropZoneProps) => {
|
}: DropZoneProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { maxFileSize } = useSpreadsheetImportInternal();
|
const { maxFileSize } = useSpreadsheetImportInternal();
|
||||||
@ -53,7 +53,7 @@ export const DropZone = ({
|
|||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
noKeyboard: true,
|
noKeyboard: true,
|
||||||
maxFiles: 1,
|
multiple: true,
|
||||||
maxSize: maxFileSize,
|
maxSize: maxFileSize,
|
||||||
onDragEnter: () => {
|
onDragEnter: () => {
|
||||||
setIsDraggingFile(true);
|
setIsDraggingFile(true);
|
||||||
@ -64,8 +64,8 @@ export const DropZone = ({
|
|||||||
onDrop: () => {
|
onDrop: () => {
|
||||||
setIsDraggingFile(false);
|
setIsDraggingFile(false);
|
||||||
},
|
},
|
||||||
onDropAccepted: async ([file]) => {
|
onDropAccepted: async (files) => {
|
||||||
onUploadFile(file);
|
onUploadFiles(files);
|
||||||
setIsDraggingFile(false);
|
setIsDraggingFile(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -85,7 +85,7 @@ export const DropZone = ({
|
|||||||
stroke={theme.icon.stroke.sm}
|
stroke={theme.icon.stroke.sm}
|
||||||
size={theme.icon.size.lg}
|
size={theme.icon.size.lg}
|
||||||
/>
|
/>
|
||||||
<StyledUploadDragTitle>Upload a file</StyledUploadDragTitle>
|
<StyledUploadDragTitle>Upload files</StyledUploadDragTitle>
|
||||||
<StyledUploadDragSubTitle>
|
<StyledUploadDragSubTitle>
|
||||||
Drag and Drop Here
|
Drag and Drop Here
|
||||||
</StyledUploadDragSubTitle>
|
</StyledUploadDragSubTitle>
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export const AIChatTab = ({
|
|||||||
{isDraggingFile && (
|
{isDraggingFile && (
|
||||||
<DropZone
|
<DropZone
|
||||||
setIsDraggingFile={setIsDraggingFile}
|
setIsDraggingFile={setIsDraggingFile}
|
||||||
onUploadFile={(files) => uploadFiles([files])}
|
onUploadFiles={uploadFiles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isDraggingFile && (
|
{!isDraggingFile && (
|
||||||
|
|||||||
Reference in New Issue
Block a user