From 062bbd57a36b69111039ea87790e47a660df4c34 Mon Sep 17 00:00:00 2001
From: Jeet Desai <52026385+jeet1desai@users.noreply.github.com>
Date: Mon, 22 Jan 2024 21:30:18 +0530
Subject: [PATCH] drag and drop on files tab (#3432)
* #3345 drag and drop on files tab
* #3432 resolved comments on drag and drop feature
---
.../files/components/AttachmentList.tsx | 71 ++++++++----
.../files/components/Attachments.tsx | 101 +++++++-----------
.../activities/files/components/DropZone.tsx | 96 +++++++++++++++++
.../files/hooks/useUploadAttachmentFile.tsx | 54 ++++++++++
4 files changed, 239 insertions(+), 83 deletions(-)
create mode 100644 packages/twenty-front/src/modules/activities/files/components/DropZone.tsx
create mode 100644 packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx
diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx
index 3aa4a5dac..9e885f8b8 100644
--- a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx
+++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx
@@ -1,11 +1,15 @@
-import { ReactElement } from 'react';
+import { ReactElement, useState } from 'react';
import styled from '@emotion/styled';
+import { DropZone } from '@/activities/files/components/DropZone';
+import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { Attachment } from '@/activities/files/types/Attachment';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { AttachmentRow } from './AttachmentRow';
type AttachmentListProps = {
+ targetableObject: ActivityTargetableObject;
title: string;
attachments: Attachment[];
button?: ReactElement | false;
@@ -17,7 +21,8 @@ const StyledContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
- padding: 8px 24px;
+ padding: ${({ theme }) => theme.spacing(2, 6, 6)};
+ height: 100%;
`;
const StyledTitleBar = styled.h3`
@@ -51,26 +56,50 @@ const StyledAttachmentContainer = styled.div`
width: 100%;
`;
+const StyledDropZoneContainer = styled.div`
+ height: 100%;
+ width: 100%;
+`;
+
export const AttachmentList = ({
+ targetableObject,
title,
attachments,
button,
-}: AttachmentListProps) => (
- <>
- {attachments && attachments.length > 0 && (
-
-
-
- {title} {attachments.length}
-
- {button}
-
-
- {attachments.map((attachment) => (
-
- ))}
-
-
- )}
- >
-);
+}: AttachmentListProps) => {
+ const { uploadAttachmentFile } = useUploadAttachmentFile();
+ const [isDraggingFile, setIsDraggingFile] = useState(false);
+
+ const onUploadFile = async (file: File) => {
+ await uploadAttachmentFile(file, targetableObject);
+ };
+
+ return (
+ <>
+ {attachments && attachments.length > 0 && (
+
+
+
+ {title} {attachments.length}
+
+ {button}
+
+ setIsDraggingFile(true)}>
+ {isDraggingFile ? (
+
+ ) : (
+
+ {attachments.map((attachment) => (
+
+ ))}
+
+ )}
+
+
+ )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
index 27225559c..7b80a3d6f 100644
--- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
+++ b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
@@ -1,20 +1,14 @@
-import { ChangeEvent, useRef } from 'react';
+import { ChangeEvent, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { isNonEmptyArray } from '@sniptt/guards';
-import { useRecoilValue } from 'recoil';
import { AttachmentList } from '@/activities/files/components/AttachmentList';
+import { DropZone } from '@/activities/files/components/DropZone';
import { useAttachments } from '@/activities/files/hooks/useAttachments';
-import { Attachment } from '@/activities/files/types/Attachment';
-import { getFileType } from '@/activities/files/utils/getFileType';
+import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
-import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
-import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
-import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
-import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
-import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
const StyledTaskGroupEmptyContainer = styled.div`
align-items: center;
@@ -24,10 +18,7 @@ const StyledTaskGroupEmptyContainer = styled.div`
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
- padding-bottom: ${({ theme }) => theme.spacing(16)};
- padding-left: ${({ theme }) => theme.spacing(4)};
- padding-right: ${({ theme }) => theme.spacing(4)};
- padding-top: ${({ theme }) => theme.spacing(3)};
+ height: 100%;
`;
const StyledEmptyTaskGroupTitle = styled.div`
@@ -57,21 +48,21 @@ const StyledFileInput = styled.input`
display: none;
`;
+const StyledDropZoneContainer = styled.div`
+ height: 100%;
+ padding: ${({ theme }) => theme.spacing(6)};
+`;
+
export const Attachments = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const inputFileRef = useRef(null);
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { attachments } = useAttachments(targetableObject);
+ const { uploadAttachmentFile } = useUploadAttachmentFile();
- const [uploadFile] = useUploadFileMutation();
-
- const { createOneRecord: createOneAttachment } =
- useCreateOneRecord({
- objectNameSingular: CoreObjectNameSingular.Attachment,
- });
+ const [isDraggingFile, setIsDraggingFile] = useState(false);
const handleFileChange = (e: ChangeEvent) => {
if (e.target.files) onUploadFile?.(e.target.files[0]);
@@ -82,52 +73,37 @@ export const Attachments = ({
};
const onUploadFile = async (file: File) => {
- const result = await uploadFile({
- variables: {
- file,
- fileFolder: FileFolder.Attachment,
- },
- });
-
- const attachmentUrl = result?.data?.uploadFile;
-
- if (!attachmentUrl) {
- return;
- }
-
- const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
- nameSingular: targetableObject.targetObjectNameSingular,
- });
-
- const attachmentToCreate = {
- authorId: currentWorkspaceMember?.id,
- name: file.name,
- fullPath: attachmentUrl,
- type: getFileType(file.name),
- [targetableObjectFieldIdName]: targetableObject.id,
- };
-
- await createOneAttachment(attachmentToCreate);
+ await uploadAttachmentFile(file, targetableObject);
};
if (!isNonEmptyArray(attachments)) {
return (
-
-
-
- No files yet
- Upload one:
-
-
+ setIsDraggingFile(true)}>
+ {isDraggingFile ? (
+
+ ) : (
+
+
+ No files yet
+
+ Upload one:
+
+
+
+ )}
+
);
}
@@ -139,6 +115,7 @@ export const Attachments = ({
type="file"
/>
theme.background.secondary};
+ border: ${({ theme }) => `2px dashed ${theme.border.color.strong}`};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ text-align: center;
+`;
+
+const StyledUploadDragTitle = styled.div`
+ color: ${({ theme }) => theme.font.color.primary};
+ font-size: ${({ theme }) => theme.font.size.lg};
+ font-weight: ${({ theme }) => theme.font.weight.medium};
+ line-height: ${({ theme }) => theme.text.lineHeight.md};
+ margin-bottom: 8px;
+`;
+
+const StyledUploadDragSubTitle = styled.div`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ font-size: ${({ theme }) => theme.font.size.md};
+ font-weight: ${({ theme }) => theme.font.weight.regular};
+ line-height: ${({ theme }) => theme.text.lineHeight.md};
+`;
+
+const StyledUploadIcon = styled(IconUpload)`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ margin-bottom: ${({ theme }) => theme.spacing(3)};
+`;
+
+type DropZoneProps = {
+ setIsDraggingFile: (drag: boolean) => void;
+ onUploadFile: (file: File) => void;
+};
+
+export const DropZone = ({
+ setIsDraggingFile,
+ onUploadFile,
+}: DropZoneProps) => {
+ const theme = useTheme();
+ const { maxFileSize } = useSpreadsheetImportInternal();
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ noClick: true,
+ noKeyboard: true,
+ maxFiles: 1,
+ maxSize: maxFileSize,
+ onDragEnter: () => {
+ setIsDraggingFile(true);
+ },
+ onDragLeave: () => {
+ setIsDraggingFile(false);
+ },
+ onDrop: () => {
+ setIsDraggingFile(false);
+ },
+ onDropAccepted: async ([file]) => {
+ onUploadFile(file);
+ setIsDraggingFile(false);
+ },
+ });
+
+ return (
+
+ {isDragActive && (
+ <>
+
+
+ Upload a file
+
+ Drag and Drop Here
+
+ >
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx
new file mode 100644
index 000000000..7fbdd8fb8
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx
@@ -0,0 +1,54 @@
+import { useRecoilValue } from 'recoil';
+
+import { getFileType } from '@/activities/files/utils/getFileType';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
+import { Attachment } from '@/attachments/types/Attachment';
+import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
+import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
+
+export const useUploadAttachmentFile = () => {
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
+ const [uploadFile] = useUploadFileMutation();
+
+ const { createOneRecord: createOneAttachment } =
+ useCreateOneRecord({
+ objectNameSingular: CoreObjectNameSingular.Attachment,
+ });
+
+ const uploadAttachmentFile = async (
+ file: File,
+ targetableObject: ActivityTargetableObject,
+ ) => {
+ const result = await uploadFile({
+ variables: {
+ file,
+ fileFolder: FileFolder.Attachment,
+ },
+ });
+
+ const attachmentUrl = result?.data?.uploadFile;
+
+ if (!attachmentUrl) {
+ return;
+ }
+
+ const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
+ const attachmentToCreate = {
+ authorId: currentWorkspaceMember?.id,
+ name: file.name,
+ fullPath: attachmentUrl,
+ type: getFileType(file.name),
+ [targetableObjectFieldIdName]: targetableObject.id,
+ };
+
+ await createOneAttachment(attachmentToCreate);
+ };
+
+ return { uploadAttachmentFile };
+};