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" />
|
<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>
|
</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
|
// @flow
|
||||||
import React from 'react';
|
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 PAGES from 'constants/pages';
|
||||||
import * as PUBLISH_TYPES from 'constants/publish_types';
|
import * as PUBLISH_TYPES from 'constants/publish_types';
|
||||||
import useDragDrop from 'effects/use-drag-drop';
|
import Icon from 'component/common/icon';
|
||||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
|||||||
import classnames from 'classnames';
|
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 { getTree } from 'util/web-file-system';
|
||||||
import { withRouter } from 'react-router';
|
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 = {
|
type Props = {
|
||||||
// Lazy fix for flow errors:
|
// 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}`;
|
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) {
|
function FileDrop(props: Props) {
|
||||||
const { history, filePath, updatePublishForm } = props;
|
const { history, filePath, updatePublishForm } = props;
|
||||||
const { drag, dropData } = useDragDrop();
|
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]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
// Handle drop...
|
// Handle drop...
|
||||||
if (dropData) {
|
if (dropData) {
|
||||||
getTree(dropData)
|
getTree(dropData)
|
||||||
.then(entries => {
|
.then(entries => {
|
||||||
|
if (entries && entries.length) {
|
||||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
|||||||
setFiles(entries);
|
setFiles(entries);
|
||||||
|
}
|
||||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
// Invalid entry / entries
|
// 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 (!drag && files.length) {
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
// Handle single file publish
|
// Handle single file publish
|
||||||
files[0].entry.file(webFile => {
|
setSelectedFile(files[0]);
|
||||||
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(webFile);
|
updatePublishForm({ filePath: { publish: PUBLISH_TYPES.DROP, webFile: files[0] } });
|
||||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
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.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle files
|
// Handle files
|
||||||
}, [drag, files, error]);
|
}, [drag, files, error, updatePublishForm, setSelectedFile]);
|
||||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
|||||||
|
|
||||||
// Wait for publish state update:
|
// Wait for publish state update:
|
||||||
React.useEffect(() => {
|
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();
|
navigateToPublish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [filePath, selectedFile, navigateToPublish]);
|
}, [filePath, selectedFile, navigateToPublish, setFiles]);
|
||||||
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 multipleFiles = files.length > 1;
|
||||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
|||||||
return (
|
return (
|
||||||
<div className={classnames('file-drop', show && 'file-drop--show')}>
|
<div className={classnames('file-drop', show && 'file-drop--show')}>
|
||||||
<p>Drop your files here!</p>
|
<div className={classnames('card', 'file-drop__area')}>
|
||||||
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
Please wrap this in Please wrap this in `__()`
ok, done. ok, done.
|
|||||||
{files.map(({ entry }) => (
|
<Icon size={64} icon={multipleFiles ? ICONS.ALERT : ICONS.PUBLISH} className={'main-icon'} />
|
||||||
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 key={entry.name}>{entry.name}</div>
|
<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.
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.
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>
|
</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 SCIENCE = 'Science';
|
||||||
export const ANALYTICS = 'BarChart2';
|
export const ANALYTICS = 'BarChart2';
|
||||||
export const PURCHASED = 'Key';
|
export const PURCHASED = 'Key';
|
||||||
|
export const CIRCLE = 'Circle';
|
||||||
|
|
|
@ -11,9 +11,66 @@
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&.file-drop--show {
|
&.file-drop--show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
transition: opacity 0.3s ease;
|
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) {
|
if (item.kind === 'file' && item.webkitGetAsEntry) {
|
||||||
return 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
|
// Read entries from directory
|
||||||
const readDirectory = directory => {
|
const readDirectory = directory => {
|
||||||
|
// Some browsers don't support this
|
||||||
|
if (directory.createReader !== undefined) {
|
||||||
let dirReader = directory.createReader();
|
let dirReader = directory.createReader();
|
||||||
|
return new Promise((resolve, reject) => dirReader.readEntries(resolve, reject));
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
dirReader.readEntries(
|
|
||||||
results => {
|
|
||||||
if (results.length) {
|
|
||||||
resolve(results);
|
|
||||||
} else {
|
|
||||||
reject();
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
error => reject(error)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get file system entries from the dataTransfer items list:
|
// Get file system entries from the dataTransfer items list:
|
||||||
export const getFiles = dataTransfer => {
|
|
||||||
|
const getFiles = (items, directoryEntries) => {
|
||||||
let entries = [];
|
let entries = [];
|
||||||
const { items } = dataTransfer;
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let item of items) {
|
||||||
const entry = getAsEntry(items[i]);
|
const entry = directoryEntries ? item : getAsEntry(item);
|
||||||
if (entry !== null && entry.isFile) {
|
if (entry && entry.isFile) {
|
||||||
entries.push({ entry });
|
const file = getFile(entry);
|
||||||
|
entries.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entries;
|
|
||||||
|
return Promise.all(entries);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a valid file tree from dataTransfer:
|
// Generate a valid file tree from dataTransfer:
|
||||||
// - Ignores directory entries
|
// - Ignores directory entries
|
||||||
// - Ignores recursive search
|
// - Ignores recursive search
|
||||||
export const getTree = async dataTransfer => {
|
export const getTree = async dataTransfer => {
|
||||||
let tree = [];
|
|
||||||
if (dataTransfer) {
|
if (dataTransfer) {
|
||||||
const { items, files } = dataTransfer;
|
const { items, files } = dataTransfer;
|
||||||
// Handle single item drop
|
// Handle single item drop
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
const entry = getAsEntry(items[0]);
|
const root = getAsEntry(items[0]);
|
||||||
// Handle entry
|
// Handle entry
|
||||||
if (entry) {
|
if (root) {
|
||||||
const root = { entry };
|
|
||||||
// Handle directory
|
// Handle directory
|
||||||
if (entry.isDirectory) {
|
if (root.isDirectory) {
|
||||||
const directoryEntries = await readDirectory(entry);
|
const directoryEntries = await readDirectory(root);
|
||||||
directoryEntries.forEach(item => {
|
// Get each file from the list
|
||||||
if (item.isFile) {
|
return getFiles(directoryEntries, true);
|
||||||
tree.push({ entry: item, rootPath: root.path });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Hanlde file
|
// Hanlde file
|
||||||
if (entry.isFile) {
|
if (root.isFile) {
|
||||||
tree.push(root);
|
const file = await getFile(root);
|
||||||
|
return [file];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle multiple items drop
|
// Handle multiple items drop
|
||||||
if (files.length > 1) {
|
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
__()