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 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 { Attachment } from '@/activities/files/types/Attachment';
|
||||||
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
|
||||||
import { AttachmentRow } from './AttachmentRow';
|
import { AttachmentRow } from './AttachmentRow';
|
||||||
|
|
||||||
type AttachmentListProps = {
|
type AttachmentListProps = {
|
||||||
|
targetableObject: ActivityTargetableObject;
|
||||||
title: string;
|
title: string;
|
||||||
attachments: Attachment[];
|
attachments: Attachment[];
|
||||||
button?: ReactElement | false;
|
button?: ReactElement | false;
|
||||||
@ -17,7 +21,8 @@ const StyledContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 8px 24px;
|
padding: ${({ theme }) => theme.spacing(2, 6, 6)};
|
||||||
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTitleBar = styled.h3`
|
const StyledTitleBar = styled.h3`
|
||||||
@ -51,26 +56,50 @@ const StyledAttachmentContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledDropZoneContainer = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
export const AttachmentList = ({
|
export const AttachmentList = ({
|
||||||
|
targetableObject,
|
||||||
title,
|
title,
|
||||||
attachments,
|
attachments,
|
||||||
button,
|
button,
|
||||||
}: AttachmentListProps) => (
|
}: AttachmentListProps) => {
|
||||||
<>
|
const { uploadAttachmentFile } = useUploadAttachmentFile();
|
||||||
{attachments && attachments.length > 0 && (
|
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||||
<StyledContainer>
|
|
||||||
<StyledTitleBar>
|
const onUploadFile = async (file: File) => {
|
||||||
<StyledTitle>
|
await uploadAttachmentFile(file, targetableObject);
|
||||||
{title} <StyledCount>{attachments.length}</StyledCount>
|
};
|
||||||
</StyledTitle>
|
|
||||||
{button}
|
return (
|
||||||
</StyledTitleBar>
|
<>
|
||||||
<StyledAttachmentContainer>
|
{attachments && attachments.length > 0 && (
|
||||||
{attachments.map((attachment) => (
|
<StyledContainer>
|
||||||
<AttachmentRow key={attachment.id} attachment={attachment} />
|
<StyledTitleBar>
|
||||||
))}
|
<StyledTitle>
|
||||||
</StyledAttachmentContainer>
|
{title} <StyledCount>{attachments.length}</StyledCount>
|
||||||
</StyledContainer>
|
</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 styled from '@emotion/styled';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { AttachmentList } from '@/activities/files/components/AttachmentList';
|
import { AttachmentList } from '@/activities/files/components/AttachmentList';
|
||||||
|
import { DropZone } from '@/activities/files/components/DropZone';
|
||||||
import { useAttachments } from '@/activities/files/hooks/useAttachments';
|
import { useAttachments } from '@/activities/files/hooks/useAttachments';
|
||||||
import { Attachment } from '@/activities/files/types/Attachment';
|
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
||||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
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 { IconPlus } from '@/ui/display/icon';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
|
|
||||||
|
|
||||||
const StyledTaskGroupEmptyContainer = styled.div`
|
const StyledTaskGroupEmptyContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -24,10 +18,7 @@ const StyledTaskGroupEmptyContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(16)};
|
height: 100%;
|
||||||
padding-left: ${({ theme }) => theme.spacing(4)};
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
|
||||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledEmptyTaskGroupTitle = styled.div`
|
const StyledEmptyTaskGroupTitle = styled.div`
|
||||||
@ -57,21 +48,21 @@ const StyledFileInput = styled.input`
|
|||||||
display: none;
|
display: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledDropZoneContainer = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
|
`;
|
||||||
|
|
||||||
export const Attachments = ({
|
export const Attachments = ({
|
||||||
targetableObject,
|
targetableObject,
|
||||||
}: {
|
}: {
|
||||||
targetableObject: ActivityTargetableObject;
|
targetableObject: ActivityTargetableObject;
|
||||||
}) => {
|
}) => {
|
||||||
const inputFileRef = useRef<HTMLInputElement>(null);
|
const inputFileRef = useRef<HTMLInputElement>(null);
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
|
||||||
const { attachments } = useAttachments(targetableObject);
|
const { attachments } = useAttachments(targetableObject);
|
||||||
|
const { uploadAttachmentFile } = useUploadAttachmentFile();
|
||||||
|
|
||||||
const [uploadFile] = useUploadFileMutation();
|
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||||
|
|
||||||
const { createOneRecord: createOneAttachment } =
|
|
||||||
useCreateOneRecord<Attachment>({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files) onUploadFile?.(e.target.files[0]);
|
if (e.target.files) onUploadFile?.(e.target.files[0]);
|
||||||
@ -82,52 +73,37 @@ export const Attachments = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onUploadFile = async (file: File) => {
|
const onUploadFile = async (file: File) => {
|
||||||
const result = await uploadFile({
|
await uploadAttachmentFile(file, targetableObject);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isNonEmptyArray(attachments)) {
|
if (!isNonEmptyArray(attachments)) {
|
||||||
return (
|
return (
|
||||||
<StyledTaskGroupEmptyContainer>
|
<StyledDropZoneContainer onDragEnter={() => setIsDraggingFile(true)}>
|
||||||
<StyledFileInput
|
{isDraggingFile ? (
|
||||||
ref={inputFileRef}
|
<DropZone
|
||||||
onChange={handleFileChange}
|
setIsDraggingFile={setIsDraggingFile}
|
||||||
type="file"
|
onUploadFile={onUploadFile}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
<StyledEmptyTaskGroupTitle>No files yet</StyledEmptyTaskGroupTitle>
|
<StyledTaskGroupEmptyContainer>
|
||||||
<StyledEmptyTaskGroupSubTitle>Upload one:</StyledEmptyTaskGroupSubTitle>
|
<StyledFileInput
|
||||||
<Button
|
ref={inputFileRef}
|
||||||
Icon={IconPlus}
|
onChange={handleFileChange}
|
||||||
title="Add file"
|
type="file"
|
||||||
variant="secondary"
|
/>
|
||||||
onClick={handleUploadFileClick}
|
<StyledEmptyTaskGroupTitle>No files yet</StyledEmptyTaskGroupTitle>
|
||||||
/>
|
<StyledEmptyTaskGroupSubTitle>
|
||||||
</StyledTaskGroupEmptyContainer>
|
Upload one:
|
||||||
|
</StyledEmptyTaskGroupSubTitle>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="Add file"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleUploadFileClick}
|
||||||
|
/>
|
||||||
|
</StyledTaskGroupEmptyContainer>
|
||||||
|
)}
|
||||||
|
</StyledDropZoneContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +115,7 @@ export const Attachments = ({
|
|||||||
type="file"
|
type="file"
|
||||||
/>
|
/>
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
|
targetableObject={targetableObject}
|
||||||
title="All"
|
title="All"
|
||||||
attachments={attachments ?? []}
|
attachments={attachments ?? []}
|
||||||
button={
|
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