Merge pull request #682 from ONLYOFFICE/feature/upload-empty-folders
Feature/upload empty folders
This commit is contained in:
commit
5795586f0d
@ -0,0 +1,190 @@
|
||||
function toFileWithPath(file, path) {
|
||||
if (typeof file?.path === "string") return file;
|
||||
|
||||
// on electron, path is already set to the absolute path
|
||||
const { webkitRelativePath } = file;
|
||||
Object.defineProperty(file, "path", {
|
||||
value:
|
||||
typeof path === "string"
|
||||
? path
|
||||
: // If <input webkitdirectory> is set,
|
||||
// the File will have a {webkitRelativePath} property
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
|
||||
typeof webkitRelativePath === "string" && webkitRelativePath.length > 0
|
||||
? webkitRelativePath
|
||||
: file.name,
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
const FILES_TO_IGNORE = [
|
||||
// Thumbnail cache files for macOS and Windows
|
||||
".DS_Store",
|
||||
"Thumbs.db", // Windows
|
||||
];
|
||||
/**
|
||||
* Convert a DragEvent's DataTrasfer object to a list of File objects
|
||||
* NOTE: If some of the items are folders,
|
||||
* everything will be flattened and placed in the same list but the paths will be kept as a {path} property.
|
||||
* @param evt
|
||||
*/
|
||||
export default async function getFilesFromEvent(evt) {
|
||||
return isDragEvt(evt) && evt.dataTransfer
|
||||
? getDataTransferFiles(evt.dataTransfer, evt.type)
|
||||
: getInputFiles(evt);
|
||||
}
|
||||
|
||||
function isDragEvt(value) {
|
||||
return !!value.dataTransfer;
|
||||
}
|
||||
|
||||
function getInputFiles(evt) {
|
||||
const files = isInput(evt.target)
|
||||
? evt.target.files
|
||||
? fromList(evt.target.files)
|
||||
: []
|
||||
: [];
|
||||
return files.map((file) => toFileWithPath(file));
|
||||
}
|
||||
|
||||
function isInput(value) {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
async function getDataTransferFiles(dt, type) {
|
||||
// IE11 does not support dataTransfer.items
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items#Browser_compatibility
|
||||
if (dt.items) {
|
||||
const items = fromList(dt.items).filter((item) => item.kind === "file");
|
||||
// According to https://html.spec.whatwg.org/multipage/dnd.html#dndevents,
|
||||
// only 'dragstart' and 'drop' has access to the data (source node)
|
||||
if (type !== "drop") {
|
||||
return items;
|
||||
}
|
||||
const files = await Promise.all(items.map(toFilePromises));
|
||||
return noIgnoredFiles(flatten(files));
|
||||
}
|
||||
return noIgnoredFiles(fromList(dt.files).map((file) => toFileWithPath(file)));
|
||||
}
|
||||
|
||||
function noIgnoredFiles(files) {
|
||||
return files.filter((file) => FILES_TO_IGNORE.indexOf(file.name) === -1);
|
||||
}
|
||||
|
||||
// IE11 does not support Array.from()
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Browser_compatibility
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FileList
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItemList
|
||||
function fromList(items) {
|
||||
const files = [];
|
||||
// tslint:disable: prefer-for-of
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const file = items[i];
|
||||
files.push(file);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem
|
||||
function toFilePromises(item) {
|
||||
if (typeof item.webkitGetAsEntry !== "function") {
|
||||
return fromDataTransferItem(item);
|
||||
}
|
||||
const entry = item.webkitGetAsEntry();
|
||||
// Safari supports dropping an image node from a different window and can be retrieved using
|
||||
// the DataTransferItem.getAsFile() API
|
||||
// NOTE: FileSystemEntry.file() throws if trying to get the file
|
||||
if (entry && entry.isDirectory) {
|
||||
return fromDirEntry(entry);
|
||||
}
|
||||
return fromDataTransferItem(item);
|
||||
}
|
||||
|
||||
function flatten(items) {
|
||||
return items.reduce(
|
||||
(acc, files) => [
|
||||
...acc,
|
||||
...(Array.isArray(files) ? flatten(files) : [files]),
|
||||
],
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
function fromDataTransferItem(item) {
|
||||
const file = item.getAsFile();
|
||||
if (!file) {
|
||||
return Promise.reject(`${item} is not a File`);
|
||||
}
|
||||
const fwp = toFileWithPath(file);
|
||||
return Promise.resolve(fwp);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemEntry
|
||||
async function fromEntry(entry) {
|
||||
return entry.isDirectory ? fromDirEntry(entry) : fromFileEntry(entry);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry
|
||||
function fromDirEntry(entry) {
|
||||
const reader = entry.createReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
const entries = [];
|
||||
let empty = true;
|
||||
function readEntries() {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry/createReader
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries
|
||||
reader.readEntries(
|
||||
async (batch) => {
|
||||
if (!batch.length) {
|
||||
// Done reading directory
|
||||
try {
|
||||
const files = await Promise.all(entries);
|
||||
if (empty) {
|
||||
files.push([createEmptyDirFile(entry)]);
|
||||
}
|
||||
resolve(files);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
const items = Promise.all(batch.map(fromEntry));
|
||||
entries.push(items);
|
||||
// Continue reading
|
||||
empty = false;
|
||||
readEntries();
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
readEntries();
|
||||
});
|
||||
}
|
||||
|
||||
function createEmptyDirFile(entry) {
|
||||
const file = new File([], entry.name);
|
||||
const fwp = toFileWithPath(file, entry.fullPath + "/");
|
||||
|
||||
Object.defineProperty(fwp, "isEmptyDirectory", {
|
||||
value: true,
|
||||
});
|
||||
return fwp;
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileEntry
|
||||
async function fromFileEntry(entry) {
|
||||
return new Promise((resolve, reject) => {
|
||||
entry.file(
|
||||
(file) => {
|
||||
const fwp = toFileWithPath(file, entry.fullPath);
|
||||
resolve(fwp);
|
||||
},
|
||||
(err) => {
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import getFilesFromEvent from "./get-files-from-event";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import StyledDragAndDrop from "./styled-drag-and-drop";
|
||||
@ -8,7 +9,7 @@ const DragAndDrop = (props) => {
|
||||
const { isDropZone, children, dragging, className, ...rest } = props;
|
||||
const classNameProp = className ? className : "";
|
||||
|
||||
const onDrop = (acceptedFiles, array) => {
|
||||
const onDrop = (acceptedFiles) => {
|
||||
acceptedFiles.length && props.onDrop && props.onDrop(acceptedFiles);
|
||||
};
|
||||
|
||||
@ -25,6 +26,7 @@ const DragAndDrop = (props) => {
|
||||
onDrop,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
getFilesFromEvent: (event) => getFilesFromEvent(event),
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -30,7 +30,7 @@
|
||||
"react-countdown": "2.3.2",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-device-detect": "^1.17.0",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-dropzone": "^11.4.2",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
"react-onclickoutside": "^6.11.2",
|
||||
"react-svg": "^12.1.0",
|
||||
|
@ -34,10 +34,26 @@ export default function withFileActions(WrappedFileItem) {
|
||||
};
|
||||
|
||||
onDropZoneUpload = (files, uploadToFolder) => {
|
||||
const { t, dragging, setDragging, startUpload } = this.props;
|
||||
const {
|
||||
t,
|
||||
dragging,
|
||||
setDragging,
|
||||
startUpload,
|
||||
uploadEmptyFolders,
|
||||
} = this.props;
|
||||
|
||||
dragging && setDragging(false);
|
||||
startUpload(files, uploadToFolder, t);
|
||||
|
||||
const emptyFolders = files.filter((f) => f.isEmptyDirectory);
|
||||
|
||||
if (emptyFolders.length > 0) {
|
||||
uploadEmptyFolders(emptyFolders, uploadToFolder).then(() => {
|
||||
const onlyFiles = files.filter((f) => !f.isEmptyDirectory);
|
||||
if (onlyFiles.length > 0) startUpload(onlyFiles, uploadToFolder, t);
|
||||
});
|
||||
} else {
|
||||
startUpload(files, uploadToFolder, t);
|
||||
}
|
||||
};
|
||||
|
||||
onDrop = (items) => {
|
||||
@ -247,6 +263,7 @@ export default function withFileActions(WrappedFileItem) {
|
||||
onSelectItem,
|
||||
setNewBadgeCount,
|
||||
openFileAction,
|
||||
uploadEmptyFolders,
|
||||
} = filesActionsStore;
|
||||
const { setSharingPanelVisible } = dialogsStore;
|
||||
const {
|
||||
@ -315,6 +332,7 @@ export default function withFileActions(WrappedFileItem) {
|
||||
dragging,
|
||||
setDragging,
|
||||
startUpload,
|
||||
uploadEmptyFolders,
|
||||
draggable,
|
||||
setTooltipPosition,
|
||||
setStartDrag,
|
||||
|
@ -27,6 +27,7 @@ const Item = ({
|
||||
onBadgeClick,
|
||||
showDragItems,
|
||||
startUpload,
|
||||
uploadEmptyFolders,
|
||||
setDragging,
|
||||
}) => {
|
||||
const [isDragActive, setIsDragActive] = React.useState(false);
|
||||
@ -41,9 +42,18 @@ const Item = ({
|
||||
const onDropZoneUpload = React.useCallback(
|
||||
(files, uploadToFolder) => {
|
||||
dragging && setDragging(false);
|
||||
startUpload(files, uploadToFolder, t);
|
||||
const emptyFolders = files.filter((f) => f.isEmptyDirectory);
|
||||
|
||||
if (emptyFolders.length > 0) {
|
||||
uploadEmptyFolders(emptyFolders, uploadToFolder).then(() => {
|
||||
const onlyFiles = files.filter((f) => !f.isEmptyDirectory);
|
||||
if (onlyFiles.length > 0) startUpload(onlyFiles, uploadToFolder, t);
|
||||
});
|
||||
} else {
|
||||
startUpload(files, uploadToFolder, t);
|
||||
}
|
||||
},
|
||||
[t, dragging, setDragging, startUpload]
|
||||
[t, dragging, setDragging, startUpload, uploadEmptyFolders]
|
||||
);
|
||||
|
||||
const onDrop = React.useCallback(
|
||||
@ -118,6 +128,7 @@ const Items = ({
|
||||
dragging,
|
||||
setDragging,
|
||||
startUpload,
|
||||
uploadEmptyFolders,
|
||||
|
||||
isAdmin,
|
||||
myId,
|
||||
@ -273,6 +284,7 @@ const Items = ({
|
||||
t={t}
|
||||
setDragging={setDragging}
|
||||
startUpload={startUpload}
|
||||
uploadEmptyFolders={uploadEmptyFolders}
|
||||
item={item}
|
||||
dragging={dragging}
|
||||
getFolderIcon={getFolderIcon}
|
||||
@ -301,6 +313,7 @@ const Items = ({
|
||||
showText,
|
||||
setDragging,
|
||||
startUpload,
|
||||
uploadEmptyFolders,
|
||||
]
|
||||
);
|
||||
|
||||
@ -337,7 +350,7 @@ export default inject(
|
||||
} = treeFoldersStore;
|
||||
|
||||
const { id } = selectedFolderStore;
|
||||
|
||||
const { moveDragItems, uploadEmptyFolders } = filesActionsStore;
|
||||
return {
|
||||
isAdmin: auth.isAdmin,
|
||||
myId: myFolderId,
|
||||
@ -352,8 +365,9 @@ export default inject(
|
||||
dragging,
|
||||
setDragging,
|
||||
setStartDrag,
|
||||
moveDragItems: filesActionsStore.moveDragItems,
|
||||
moveDragItems,
|
||||
startUpload,
|
||||
uploadEmptyFolders,
|
||||
};
|
||||
}
|
||||
)(withTranslation(["Home", "Common", "Translations"])(observer(Items)));
|
||||
|
@ -205,9 +205,24 @@ class PureHome extends React.Component {
|
||||
};
|
||||
|
||||
onDrop = (files, uploadToFolder) => {
|
||||
const { t, startUpload, setDragging, dragging } = this.props;
|
||||
const {
|
||||
t,
|
||||
startUpload,
|
||||
setDragging,
|
||||
dragging,
|
||||
uploadEmptyFolders,
|
||||
} = this.props;
|
||||
dragging && setDragging(false);
|
||||
startUpload(files, uploadToFolder, t);
|
||||
const emptyFolders = files.filter((f) => f.isEmptyDirectory);
|
||||
|
||||
if (emptyFolders.length > 0) {
|
||||
uploadEmptyFolders(emptyFolders, uploadToFolder).then(() => {
|
||||
const onlyFiles = files.filter((f) => !f.isEmptyDirectory);
|
||||
if (onlyFiles.length > 0) startUpload(onlyFiles, uploadToFolder, t);
|
||||
});
|
||||
} else {
|
||||
startUpload(files, uploadToFolder, t);
|
||||
}
|
||||
};
|
||||
|
||||
showOperationToast = (type, qty, title) => {
|
||||
@ -395,6 +410,7 @@ export default inject(
|
||||
treeFoldersStore,
|
||||
mediaViewerDataStore,
|
||||
settingsStore,
|
||||
filesActionsStore,
|
||||
}) => {
|
||||
const {
|
||||
secondaryProgressDataStore,
|
||||
@ -453,6 +469,8 @@ export default inject(
|
||||
converted,
|
||||
} = uploadDataStore;
|
||||
|
||||
const { uploadEmptyFolders } = filesActionsStore;
|
||||
|
||||
const selectionLength = isProgressFinished ? selection.length : null;
|
||||
const selectionTitle = isProgressFinished
|
||||
? filesStore.selectionTitle
|
||||
@ -511,6 +529,7 @@ export default inject(
|
||||
setUploadPanelVisible,
|
||||
setSelections,
|
||||
startUpload,
|
||||
uploadEmptyFolders,
|
||||
isHeaderVisible: auth.settingsStore.isHeaderVisible,
|
||||
setHeaderVisible: auth.settingsStore.setHeaderVisible,
|
||||
personal: auth.settingsStore.personal,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
markAsRead,
|
||||
removeFiles,
|
||||
removeShareFiles,
|
||||
createFolder,
|
||||
} from "@appserver/common/api/files";
|
||||
import {
|
||||
ConflictResolveType,
|
||||
@ -108,6 +109,76 @@ class FilesActionStore {
|
||||
});
|
||||
};
|
||||
|
||||
convertToTree = (folders) => {
|
||||
let result = [];
|
||||
let level = { result };
|
||||
try {
|
||||
folders.forEach((folder) => {
|
||||
folder.path
|
||||
.split("/")
|
||||
.filter((name) => name !== "")
|
||||
.reduce((r, name, i, a) => {
|
||||
if (!r[name]) {
|
||||
r[name] = { result: [] };
|
||||
r.result.push({ name, children: r[name].result });
|
||||
}
|
||||
|
||||
return r[name];
|
||||
}, level);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("convertToTree", e);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
createFolderTree = async (treeList, parentFolderId) => {
|
||||
if (!treeList || !treeList.length) return;
|
||||
|
||||
for (let i = 0; i < treeList.length; i++) {
|
||||
const treeNode = treeList[i];
|
||||
|
||||
// console.log(
|
||||
// `createFolderTree parent id = ${parentFolderId} name '${treeNode.name}': `,
|
||||
// treeNode.children
|
||||
// );
|
||||
|
||||
const folder = await createFolder(parentFolderId, treeNode.name);
|
||||
const parentId = folder.id;
|
||||
|
||||
if (treeNode.children.length == 0) continue;
|
||||
|
||||
await this.createFolderTree(treeNode.children, parentId);
|
||||
}
|
||||
};
|
||||
|
||||
uploadEmptyFolders = async (emptyFolders, folderId) => {
|
||||
//console.log("uploadEmptyFolders", emptyFolders, folderId);
|
||||
|
||||
const { secondaryProgressDataStore } = this.uploadDataStore;
|
||||
const {
|
||||
setSecondaryProgressBarData,
|
||||
clearSecondaryProgressData,
|
||||
} = secondaryProgressDataStore;
|
||||
|
||||
const toFolderId = folderId ? folderId : this.selectedFolderStore.id;
|
||||
|
||||
setSecondaryProgressBarData({
|
||||
icon: "file",
|
||||
visible: true,
|
||||
percent: 0,
|
||||
label: "",
|
||||
alert: false,
|
||||
});
|
||||
|
||||
const tree = this.convertToTree(emptyFolders);
|
||||
await this.createFolderTree(tree, toFolderId);
|
||||
|
||||
this.updateCurrentFolder(null, [folderId]);
|
||||
|
||||
setTimeout(() => clearSecondaryProgressData(), TIMEOUT);
|
||||
};
|
||||
|
||||
deleteAction = async (
|
||||
translations,
|
||||
newSelection = null,
|
||||
|
@ -16713,7 +16713,7 @@ react-draggable@^4.4.3:
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-dropzone@^11.2.4:
|
||||
react-dropzone@^11.4.2:
|
||||
version "11.7.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356"
|
||||
integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==
|
||||
|
Loading…
Reference in New Issue
Block a user