Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the upload of empty folder on drop #2164

Merged
merged 16 commits into from
Jun 10, 2022
Merged
30 changes: 27 additions & 3 deletions packages/common-components/src/FileInput/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Button } from "../Button"
import { Typography } from "../Typography"
import { PlusIcon, CrossIcon } from "../Icons"
import { ScrollbarWrapper } from "../ScrollbarWrapper"
import { getFilesAndEmptyDirFromDataTransferItems } from "../utils"

const useStyles = makeStyles(({ constants, palette, overrides }: ITheme) =>
createStyles({
Expand Down Expand Up @@ -118,6 +119,7 @@ interface IFileInputProps extends DropzoneOptions {
error?: string
}
onFileNumberChange: (filesNumber: number) => void
onEmptyFolderChange?: (emptyFolderPaths: string[]) => void
Tbaut marked this conversation as resolved.
Show resolved Hide resolved
moreFilesLabel: string
testId?: string
}
Expand All @@ -132,6 +134,7 @@ const FileInput = ({
maxFileSize,
classNames,
onFileNumberChange,
onEmptyFolderChange,
moreFilesLabel,
testId,
...props
Expand All @@ -145,6 +148,13 @@ const FileInput = ({
onFileNumberChange && onFileNumberChange(value.length)
}, [onFileNumberChange, value.length])

useEffect(() => {
// reset the field on load
helpers.setValue([])
// needed to avoid an infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const onDrop = useCallback(
async (acceptedFiles: FileWithPath[], fileRejections: FileRejection[]) => {
const filtered = acceptedFiles.filter((file) =>
Expand Down Expand Up @@ -188,9 +198,23 @@ const FileInput = ({

const { getRootProps, getInputProps, open } = useDropzone({
onDrop,
...dropZoneProps
...dropZoneProps,
getFilesFromEvent
})

async function getFilesFromEvent(event: any) {
// this is a drag n drop
// we support folder upload and empty folders
if(event.dataTransfer){
const res = await getFilesAndEmptyDirFromDataTransferItems(event.dataTransfer.items)
onEmptyFolderChange && res.emptyDirPaths?.length && onEmptyFolderChange(res.emptyDirPaths)
return res.files as File[] || []
} else {
// this is a file list using the input
return event.target.files as File[]
}
}

const removeItem = (i: number) => {
const items = value
items.splice(i, 1)
Expand Down Expand Up @@ -224,12 +248,12 @@ const FileInput = ({
>
<ScrollbarWrapper className={clsx("scrollbar")}>
<ul>
{value.map((file: any, i: number) => (
{value.map((file, i) => (
<li
className={clsx(classes.item, classNames?.item)}
key={i}
>
<span className={classes.itemText}>{file.path}</span>
<span className={classes.itemText}>{`${file.path || ""}${file.name}`}</span>
<Button
testId="remove-from-file-list"
className={clsx(classes.crossIcon, classNames?.closeIcon)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
export type FileWithPath = File & {
path: string
}

interface FilesAndDirFromDataTransferItems {
files?: FileWithPath[]
emptyDirPaths?: string[]
}

const mergeResults = (prevRes: FilesAndDirFromDataTransferItems, newRes: FilesAndDirFromDataTransferItems) => {
return {
files: [...(prevRes.files || []), ...(newRes.files || [])],
emptyDirPaths: [...(prevRes.emptyDirPaths || []), ...(newRes.emptyDirPaths || [])]
} as FilesAndDirFromDataTransferItems
}

const readFile = (entry: FileEntry, path = ""): Promise<FileWithPath> => {
return new Promise((resolve, reject) => {
entry.file((file: File) => {
Object.defineProperty(file, "path", {
value: path
})
resolve(file as FileWithPath)
}, (err: Error) => {
reject(err)
})
})
}


const dirReadEntries = (dirReader: DirectoryReader, path: string): Promise<FilesAndDirFromDataTransferItems> => {
return new Promise((resolve, reject) => {
dirReader.readEntries(async (entries: FileSystemEntry[]) => {
let res: FilesAndDirFromDataTransferItems = {}
for (const entry of entries) {
const newRes = await getFilesAndEmptyDirFromEntry(entry, path)
res = mergeResults(res, newRes)
}
resolve(res)
}, (err: Error) => {
reject(err)
})
})
}

const readDir = async (entry: DirectoryEntry, path: string) => {
const dirReader = entry.createReader()
const newPath = path + entry.name + "/"
let newFilesOrFolders: FilesAndDirFromDataTransferItems = {}
let res: FilesAndDirFromDataTransferItems = {}
do {
newFilesOrFolders = await dirReadEntries(dirReader, newPath)

res = mergeResults(res, newFilesOrFolders)
} while (newFilesOrFolders?.files && newFilesOrFolders.files.length > 0)

return res
}

const getFilesAndEmptyDirFromEntry = async (entry: FileSystemEntry, path = ""): Promise<FilesAndDirFromDataTransferItems> => {
if (entry.isFile) {
const file = await readFile(entry as FileEntry, path)
return { files: [file] }
}

if (entry.isDirectory) {
// read files inside this dir
const res = await readDir(entry as DirectoryEntry, path)

if(!res?.files || res.files.length === 0){
// this is an empty dir
return { emptyDirPaths: [`${path}${entry.name}`] }
}

return res
}

return {}
}

export const getFilesAndEmptyDirFromDataTransferItems =
async (dataTransferItems: DataTransferItemList): Promise<FilesAndDirFromDataTransferItems> => {
let result: FilesAndDirFromDataTransferItems = { files: [], emptyDirPaths: [] }
const entries: (FileSystemEntry | null)[] = []

// Pull out all entries before reading them
for (let i = 0, ii = dataTransferItems.length; i < ii; i++) {
entries.push(dataTransferItems[i].webkitGetAsEntry())
}

// Recursively read through all entries
for (const entry of entries) {
if (entry) {
const newRes = await getFilesAndEmptyDirFromEntry(entry)
result = mergeResults(result, newRes)
}
}

return result
}
1 change: 1 addition & 0 deletions packages/common-components/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./formDataHelper"
export * from "./stringUtils"
export * from "./getFilesAndEmptyDirFromDataTransferItems"
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { Crumb, useToasts, useHistory, useLocation } from "@chainsafe/common-components"
import { Crumb, useToasts, useHistory, useLocation, getFilesAndEmptyDirFromDataTransferItems } from "@chainsafe/common-components"
import { useFiles, FileSystemItem } from "../../../Contexts/FilesContext"
import {
getArrayOfPaths,
Expand All @@ -23,7 +23,6 @@ import { useUser } from "../../../Contexts/UserContext"
import { DISMISSED_SURVEY_KEY } from "../../SurveyBanner"
import { FileBrowserContext } from "../../../Contexts/FileBrowserContext"
import { parseFileContentResponse } from "../../../Utils/Helpers"
import getFilesFromDataTransferItems from "../../../Utils/getFilesFromDataTransferItems"
import { Helmet } from "react-helmet-async"

const CSFFileBrowser: React.FC<IFileBrowserModuleProps> = () => {
Expand Down Expand Up @@ -184,9 +183,15 @@ const CSFFileBrowser: React.FC<IFileBrowserModuleProps> = () => {
})
return
}
const flattenedFiles = await getFilesFromDataTransferItems(fileItems)
await uploadFiles(bucket, flattenedFiles, path)
}, [bucket, accountRestricted, storageSummary, addToast, uploadFiles])
const flattenedFiles = await getFilesAndEmptyDirFromDataTransferItems(fileItems)
await uploadFiles(bucket, flattenedFiles?.files || [], path)

//create empty dir
flattenedFiles?.emptyDirPaths?.forEach(async (folderPath) => {
await filesApiClient.addBucketDirectory(bucket.id, { path: getPathWithFile(currentPath, folderPath) })

Tbaut marked this conversation as resolved.
Show resolved Hide resolved
})
}, [bucket, accountRestricted, storageSummary, uploadFiles, addToast, filesApiClient, currentPath])

const viewFolder = useCallback((cid: string) => {
const fileSystemItem = pathContents.find(f => f.cid === cid)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { useToasts, useHistory, useLocation, Crumb, Typography, ExclamationCircleIcon, Loading } from "@chainsafe/common-components"
import {
useToasts,
useHistory,
useLocation,
Crumb,
Typography,
ExclamationCircleIcon,
Loading,
getFilesAndEmptyDirFromDataTransferItems
} from "@chainsafe/common-components"
import {
getArrayOfPaths,
getURISafePathFromArray,
Expand All @@ -22,7 +31,6 @@ import DragAndDrop from "../../../Contexts/DnDContext"
import FilesList from "./views/FilesList"
import { createStyles, makeStyles } from "@chainsafe/common-theme"
import { CSFTheme } from "../../../Themes/types"
import getFilesFromDataTransferItems from "../../../Utils/getFilesFromDataTransferItems"
import { Helmet } from "react-helmet-async"

const useStyles = makeStyles(({ constants, palette }: CSFTheme) =>
Expand Down Expand Up @@ -224,9 +232,15 @@ const SharedFileBrowser = () => {
})
return
}
const flattenedFiles = await getFilesFromDataTransferItems(fileItems)
await uploadFiles(bucket, flattenedFiles, path)
}, [bucket, accountRestricted, storageSummary, addToast, uploadFiles])
const flattenedFiles = await getFilesAndEmptyDirFromDataTransferItems(fileItems)
await uploadFiles(bucket, flattenedFiles?.files || [], path)

// create any empty dir
flattenedFiles?.emptyDirPaths?.forEach(async (folderPath) => {
await filesApiClient.addBucketDirectory(bucket.id, { path: getPathWithFile(currentPath, folderPath) })

Tbaut marked this conversation as resolved.
Show resolved Hide resolved
})
}, [bucket, accountRestricted, storageSummary, uploadFiles, addToast, filesApiClient, currentPath])

const bulkOperations: IBulkOperations = useMemo(() => ({
[CONTENT_TYPES.Directory]: ["download", "move", "delete", "share"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Trans, t } from "@lingui/macro"
import clsx from "clsx"
import { CSFTheme } from "../../../Themes/types"
import { useFileBrowser } from "../../../Contexts/FileBrowserContext"
import { getPathWithFile } from "../../../Utils/pathUtils"
import { useFilesApi } from "../../../Contexts/FilesApiContext"

const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) =>
createStyles({
Expand Down Expand Up @@ -82,6 +84,8 @@ const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => {
const [isDoneDisabled, setIsDoneDisabled] = useState(true)
const { currentPath, refreshContents, bucket } = useFileBrowser()
const { storageSummary, uploadFiles } = useFiles()
const { filesApiClient } = useFilesApi()
const [emptyFolderToCreate, setEmptyFolderToCreate] = useState<string[]>([])

const UploadSchema = useMemo(() => object().shape({
files: array().required(t`Please select a file to upload`)
Expand All @@ -108,17 +112,24 @@ const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => {
try {
close()
await uploadFiles(bucket, values.files, currentPath)

//create empty folders
emptyFolderToCreate.forEach(async (folderPath) => {
await filesApiClient.addBucketDirectory(bucket.id, { path: getPathWithFile(currentPath, folderPath) })

Tbaut marked this conversation as resolved.
Show resolved Hide resolved
})
refreshContents && refreshContents()
helpers.resetForm()
} catch (error: any) {
console.error(error)
}
helpers.setSubmitting(false)
}, [bucket, close, refreshContents, uploadFiles, currentPath])
}, [bucket, close, uploadFiles, currentPath, emptyFolderToCreate, refreshContents, filesApiClient])

const formik = useFormik({
initialValues: { files: [] },
validationSchema: UploadSchema,
enableReinitialize: true,
onSubmit
})

Expand Down Expand Up @@ -148,6 +159,7 @@ const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => {
maxSize={2 * 1024 ** 3}
name="files"
onFileNumberChange={onFileNumberChange}
onEmptyFolderChange={setEmptyFolderToCreate}
testId="fileUpload"
/>
<footer className={classes.footer}>
Expand Down
3 changes: 1 addition & 2 deletions packages/files-ui/src/Contexts/FilesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import React, { useCallback, useEffect } from "react"
import { useState } from "react"
import { decryptFile, encryptFile } from "../Utils/encryption"
import { ToastParams, useToasts } from "@chainsafe/common-components"
import { FileWithPath, ToastParams, useToasts } from "@chainsafe/common-components"
import axios, { CancelToken } from "axios"
import { plural, t } from "@lingui/macro"
import { parseFileContentResponse, readFileAsync } from "../Utils/Helpers"
Expand All @@ -22,7 +22,6 @@ import { useFilesApi } from "./FilesApiContext"
import { useUser } from "./UserContext"
import { getParentPathFromFilePath, getPathWithFile, getRelativePath } from "../Utils/pathUtils"
import { Zippable, zipSync } from "fflate"
import { FileWithPath } from "../Utils/getFilesFromDataTransferItems"

type FilesContextProps = {
children: React.ReactNode | React.ReactNode[]
Expand Down
Loading