Support drag-and-drop file publishing #4170
|
@ -623,4 +623,10 @@ export const icons = {
|
|||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.COMPLETED]: buildIcon(
|
||||
<g>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as PUBLISH_TYPES from 'constants/publish_types';
|
||||
import useDragDrop from 'effects/use-drag-drop';
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
import Icon from 'component/common/icon';
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
import classnames from 'classnames';
|
||||
import useDragDrop from 'effects/use-drag-drop';
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
import { getTree } from 'util/web-file-system';
|
||||
import { withRouter } from 'react-router';
|
||||
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
type Props = {
|
||||
// Lazy fix for flow errors:
|
||||
|
@ -25,8 +28,45 @@ type Props = {
|
|||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
},
|
||||
};
|
||||
|
||||
type FileListProps = {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
files: Array<WebFile>,
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
onSelected: string => void,
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
};
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
const PUBLISH_URL = `/$/${PAGES.PUBLISH}`;
|
||||
|
||||
function FileList(props: FileListProps) {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
const { files, onSelected } = props;
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
const radio = useRadioState();
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
React.useEffect(() => {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
if (!radio.currentId) {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
radio.first();
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
if (radio.state && radio.state !== '') {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
onSelected(radio.state);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}, [radio, onSelected]);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
return (
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<RadioGroup {...radio} aria-label="fruits">
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
{files.map((entry, index) => {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
This feels really smooth 👍 This feels really smooth 👍
|
||||
const item = radio.stops[index];
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
const selected = item && item.id === radio.currentId;
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
return (
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<label key={entry.name} className={classnames(selected && 'selected')}>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<Radio {...radio} value={entry.name} />
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<Icon size={18} selected={selected} icon={selected ? ICONS.COMPLETED : ICONS.CIRCLE} />
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<span>{entry.name}</span>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
</label>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
})}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
</RadioGroup>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
function FileDrop(props: Props) {
|
||||
const { history, filePath, updatePublishForm } = props;
|
||||
const { drag, dropData } = useDragDrop();
|
||||
|
@ -43,12 +83,23 @@ function FileDrop(props: Props) {
|
|||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}
|
||||
}, [history]);
|
||||
|
||||
const handleFileSelected = name => {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
if (files && files.length) {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
const selected = files.find(file => file.name === name);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
if (selected && selected.name !== (selectedFile && selectedFile.name)) {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
setSelectedFile(selected);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
};
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
React.useEffect(() => {
|
||||
// Handle drop...
|
||||
if (dropData) {
|
||||
getTree(dropData)
|
||||
.then(entries => {
|
||||
if (entries && entries.length) {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
setFiles(entries);
|
||||
}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
})
|
||||
.catch(error => {
|
||||
// Invalid entry / entries
|
||||
|
@ -70,14 +121,12 @@ function FileDrop(props: Props) {
|
|||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
if (!drag && files.length) {
|
||||
if (files.length === 1) {
|
||||
// Handle single file publish
|
||||
files[0].entry.file(webFile => {
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
setSelectedFile(webFile);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
updatePublishForm({ filePath: { publish: PUBLISH_TYPES.DROP, webFile } });
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
});
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
setSelectedFile(files[0]);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
updatePublishForm({ filePath: { publish: PUBLISH_TYPES.DROP, webFile: files[0] } });
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}
|
||||
}
|
||||
// Handle files
|
||||
}, [drag, files, error]);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}, [drag, files, error, updatePublishForm, setSelectedFile]);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
// Wait for publish state update:
|
||||
React.useEffect(() => {
|
||||
|
@ -91,14 +140,20 @@ function FileDrop(props: Props) {
|
|||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
navigateToPublish();
|
||||
}
|
||||
}
|
||||
}, [filePath, selectedFile, navigateToPublish]);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
}, [filePath, selectedFile, navigateToPublish, setFiles]);
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
|
||||
const multipleFiles = files.length > 1;
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
return (
|
||||
<div className={classnames('file-drop', show && 'file-drop--show')}>
|
||||
<p>Drop your files here!</p>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
{files.map(({ entry }) => (
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<div key={entry.name}>{entry.name}</div>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
))}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<div className={classnames('card', 'file-drop__area')}>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<Icon size={64} icon={multipleFiles ? ICONS.ALERT : ICONS.PUBLISH} className={'main-icon'} />
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<p>{multipleFiles ? `Only one file is allowed choose wisely` : `Drop here to publish!`} </p>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
{files && files.length > 0 && (
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<div className="file-drop__list">
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
<FileList files={files} onSelected={handleFileSelected} />
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
</div>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
)}
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
</div>
|
||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
|
@ -100,3 +100,4 @@ export const SLIDERS = 'Sliders';
|
|||
export const SCIENCE = 'Science';
|
||||
export const ANALYTICS = 'BarChart2';
|
||||
export const PURCHASED = 'Key';
|
||||
export const CIRCLE = 'Circle';
|
||||
|
|
|
@ -11,9 +11,66 @@
|
|||
z-index: 5;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.file-drop--show {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.file-drop__area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-large);
|
||||
min-width: 400px;
|
||||
|
||||
.file-drop__list {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background: var(--color-menu-background);
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-miniscule) var(--spacing-small);
|
||||
|
||||
&.selected {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
box-shadow: inset 0 0 0 3px var(--color-focus);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--spacing-small);
|
||||
opacity: 0.64;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
margin: var(--spacing-small);
|
||||
}
|
||||
p {
|
||||
margin-bottom: var(--spacing-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,72 +6,64 @@ const getAsEntry = item => {
|
|||
if (item.kind === 'file' && item.webkitGetAsEntry) {
|
||||
return item.webkitGetAsEntry();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Get file object from fileEntry
|
||||
const getFile = fileEntry => new Promise((resolve, reject) => fileEntry.file(resolve, reject));
|
||||
|
||||
// Read entries from directory
|
||||
const readDirectory = directory => {
|
||||
// Some browsers don't support this
|
||||
if (directory.createReader !== undefined) {
|
||||
let dirReader = directory.createReader();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(
|
||||
results => {
|
||||
if (results.length) {
|
||||
resolve(results);
|
||||
} else {
|
||||
reject();
|
||||
return new Promise((resolve, reject) => dirReader.readEntries(resolve, reject));
|
||||
}
|
||||
},
|
||||
error => reject(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Get file system entries from the dataTransfer items list:
|
||||
export const getFiles = dataTransfer => {
|
||||
|
||||
const getFiles = (items, directoryEntries) => {
|
||||
let entries = [];
|
||||
const { items } = dataTransfer;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = getAsEntry(items[i]);
|
||||
if (entry !== null && entry.isFile) {
|
||||
entries.push({ entry });
|
||||
|
||||
for (let item of items) {
|
||||
const entry = directoryEntries ? item : getAsEntry(item);
|
||||
if (entry && entry.isFile) {
|
||||
const file = getFile(entry);
|
||||
entries.push(file);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
|
||||
return Promise.all(entries);
|
||||
};
|
||||
|
||||
// Generate a valid file tree from dataTransfer:
|
||||
// - Ignores directory entries
|
||||
// - Ignores recursive search
|
||||
export const getTree = async dataTransfer => {
|
||||
let tree = [];
|
||||
if (dataTransfer) {
|
||||
const { items, files } = dataTransfer;
|
||||
// Handle single item drop
|
||||
if (files.length === 1) {
|
||||
const entry = getAsEntry(items[0]);
|
||||
const root = getAsEntry(items[0]);
|
||||
// Handle entry
|
||||
if (entry) {
|
||||
const root = { entry };
|
||||
if (root) {
|
||||
// Handle directory
|
||||
if (entry.isDirectory) {
|
||||
const directoryEntries = await readDirectory(entry);
|
||||
directoryEntries.forEach(item => {
|
||||
if (item.isFile) {
|
||||
tree.push({ entry: item, rootPath: root.path });
|
||||
}
|
||||
});
|
||||
if (root.isDirectory) {
|
||||
const directoryEntries = await readDirectory(root);
|
||||
// Get each file from the list
|
||||
return getFiles(directoryEntries, true);
|
||||
}
|
||||
// Hanlde file
|
||||
if (entry.isFile) {
|
||||
tree.push(root);
|
||||
if (root.isFile) {
|
||||
const file = await getFile(root);
|
||||
return [file];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle multiple items drop
|
||||
if (files.length > 1) {
|
||||
tree = tree.concat(getFiles(dataTransfer));
|
||||
// Convert items to fileEntry and get each file
|
||||
return getFiles(items);
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
|
|
Please wrap this in
__()