drag and drop on files tab (#3432)

* #3345 drag and drop on files tab

* #3432 resolved comments on drag and drop feature
This commit is contained in:
Jeet Desai
2024-01-22 21:30:18 +05:30
committed by GitHub
parent e358d677f9
commit 062bbd57a3
4 changed files with 239 additions and 83 deletions

View File

@ -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 && (
<StyledContainer>
<StyledTitleBar>
<StyledTitle>
{title} <StyledCount>{attachments.length}</StyledCount>
</StyledTitle>
{button}
</StyledTitleBar>
<StyledAttachmentContainer>
{attachments.map((attachment) => (
<AttachmentRow key={attachment.id} attachment={attachment} />
))}
</StyledAttachmentContainer>
</StyledContainer>
)}
</>
);
}: AttachmentListProps) => {
const { uploadAttachmentFile } = useUploadAttachmentFile();
const [isDraggingFile, setIsDraggingFile] = useState(false);
const onUploadFile = async (file: File) => {
await uploadAttachmentFile(file, targetableObject);
};
return (
<>
{attachments && attachments.length > 0 && (
<StyledContainer>
<StyledTitleBar>
<StyledTitle>
{title} <StyledCount>{attachments.length}</StyledCount>
</StyledTitle>
{button}
</StyledTitleBar>
<StyledDropZoneContainer onDragEnter={() => setIsDraggingFile(true)}>
{isDraggingFile ? (
<DropZone
setIsDraggingFile={setIsDraggingFile}
onUploadFile={onUploadFile}
/>
) : (
<StyledAttachmentContainer>
{attachments.map((attachment) => (
<AttachmentRow key={attachment.id} attachment={attachment} />
))}
</StyledAttachmentContainer>
)}
</StyledDropZoneContainer>
</StyledContainer>
)}
</>
);
};

View File

@ -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<HTMLInputElement>(null);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { attachments } = useAttachments(targetableObject);
const { uploadAttachmentFile } = useUploadAttachmentFile();
const [uploadFile] = useUploadFileMutation();
const { createOneRecord: createOneAttachment } =
useCreateOneRecord<Attachment>({
objectNameSingular: CoreObjectNameSingular.Attachment,
});
const [isDraggingFile, setIsDraggingFile] = useState(false);
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
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 (
<StyledTaskGroupEmptyContainer>
<StyledFileInput
ref={inputFileRef}
onChange={handleFileChange}
type="file"
/>
<StyledEmptyTaskGroupTitle>No files yet</StyledEmptyTaskGroupTitle>
<StyledEmptyTaskGroupSubTitle>Upload one:</StyledEmptyTaskGroupSubTitle>
<Button
Icon={IconPlus}
title="Add file"
variant="secondary"
onClick={handleUploadFileClick}
/>
</StyledTaskGroupEmptyContainer>
<StyledDropZoneContainer onDragEnter={() => setIsDraggingFile(true)}>
{isDraggingFile ? (
<DropZone
setIsDraggingFile={setIsDraggingFile}
onUploadFile={onUploadFile}
/>
) : (
<StyledTaskGroupEmptyContainer>
<StyledFileInput
ref={inputFileRef}
onChange={handleFileChange}
type="file"
/>
<StyledEmptyTaskGroupTitle>No files yet</StyledEmptyTaskGroupTitle>
<StyledEmptyTaskGroupSubTitle>
Upload one:
</StyledEmptyTaskGroupSubTitle>
<Button
Icon={IconPlus}
title="Add file"
variant="secondary"
onClick={handleUploadFileClick}
/>
</StyledTaskGroupEmptyContainer>
)}
</StyledDropZoneContainer>
);
}
@ -139,6 +115,7 @@ export const Attachments = ({
type="file"
/>
<AttachmentList
targetableObject={targetableObject}
title="All"
attachments={attachments ?? []}
button={

View File

@ -0,0 +1,96 @@
import { useDropzone } from 'react-dropzone';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { IconUpload } from '@/ui/display/icon';
const StyledContainer = styled.div`
align-items: center;
background: ${({ theme }) => 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 (
<StyledContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...getRootProps()}
>
{isDragActive && (
<>
<input
// eslint-disable-next-line react/jsx-props-no-spreading
{...getInputProps()}
/>
<StyledUploadIcon
stroke={theme.icon.stroke.sm}
size={theme.icon.size.lg}
/>
<StyledUploadDragTitle>Upload a file</StyledUploadDragTitle>
<StyledUploadDragSubTitle>
Drag and Drop Here
</StyledUploadDragSubTitle>
</>
)}
</StyledContainer>
);
};

View File

@ -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<Attachment>({
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 };
};