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:
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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={
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user