Skip to content

Commit b294277

Browse files
authored
Drag and Drop (codesandbox#755)
* Make files plural * Multi create modules * multifile uploading * Drag n drop functionality * Change progress message * Fix lint * Live integration for mass module creation * Add changelog info * Docs pages
1 parent a334c34 commit b294277

File tree

29 files changed

+443
-92
lines changed

29 files changed

+443
-92
lines changed

packages/app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
"circular-json": "^0.4.0",
120120
"codemirror": "^5.27.4",
121121
"codesandbox-api": "^0.0.18",
122-
"codesandbox-import-utils": "^1.2.7",
122+
"codesandbox-import-utils": "1.2.10",
123123
"color": "^0.11.4",
124124
"compare-versions": "^3.1.0",
125125
"console-feed": "^2.7.3",
@@ -186,6 +186,7 @@
186186
"react-stripe-elements": "^1.2.0",
187187
"react-tagsinput": "^3.19.0",
188188
"shelljs": "^0.7.8",
189+
"shortid": "^2.2.8",
189190
"store": "^2.0.12",
190191
"string-replace-loader": "^1.3.0",
191192
"styled-components": "^3.2.1",

packages/app/src/app/components/CodeEditor/Monaco/index.js

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,6 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
236236
await this.initializeModules(sandbox.modules);
237237
await this.openNewModel(currentModule.id, currentModule.title);
238238

239-
this.addKeyCommands();
240239
import(/* webpackChunkName: 'monaco-emmet' */ './enable-emmet').then(
241240
enableEmmet => {
242241
enableEmmet.default(editor, monaco, {});
@@ -1274,16 +1273,6 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
12741273
}
12751274
};
12761275

1277-
addKeyCommands = () => {
1278-
// Disabled, we now let keybinding manager handle this
1279-
// this.editor.addCommand(
1280-
// this.monaco.KeyMod.CtrlCmd | this.monaco.KeyCode.KEY_S, // eslint-disable-line no-bitwise
1281-
// () => {
1282-
// this.handleSaveCode();
1283-
// }
1284-
// );
1285-
};
1286-
12871276
disposeModules = (modules: Array<Module>) => {
12881277
if (this.editor) {
12891278
this.editor.setModel(null);
@@ -1350,31 +1339,34 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
13501339
) => {
13511340
// Remove the first slash, as this will otherwise create errors in monaco
13521341
const path = getModulePath(modules, directories, module.id);
1342+
if (path) {
1343+
// We need to add this as a lib specifically to Monaco, because Monaco
1344+
// tends to lose type definitions if you don't touch a file for a while.
1345+
// Related issue: https://github.com/Microsoft/monaco-editor/issues/461
1346+
const lib = this.addLib(module.code || '', path);
1347+
1348+
const mode = await this.getMode(module.title);
1349+
1350+
const model = this.monaco.editor.createModel(
1351+
module.code || '',
1352+
mode === 'javascript' ? 'typescript' : mode,
1353+
new this.monaco.Uri().with({ path, scheme: 'file' })
1354+
);
13531355

1354-
// We need to add this as a lib specifically to Monaco, because Monaco
1355-
// tends to lose type definitions if you don't touch a file for a while.
1356-
// Related issue: https://github.com/Microsoft/monaco-editor/issues/461
1357-
const lib = this.addLib(module.code || '', path);
1358-
1359-
const mode = await this.getMode(module.title);
1356+
model.updateOptions({ tabSize: this.props.settings.tabWidth });
13601357

1361-
const model = this.monaco.editor.createModel(
1362-
module.code || '',
1363-
mode === 'javascript' ? 'typescript' : mode,
1364-
new this.monaco.Uri().with({ path, scheme: 'file' })
1365-
);
1366-
1367-
model.updateOptions({ tabSize: this.props.settings.tabWidth });
1358+
modelCache[module.id] = modelCache[module.id] || {
1359+
model: null,
1360+
decorations: [],
1361+
cursorPos: null,
1362+
};
1363+
modelCache[module.id].model = model;
1364+
modelCache[module.id].lib = lib;
13681365

1369-
modelCache[module.id] = modelCache[module.id] || {
1370-
model: null,
1371-
decorations: [],
1372-
cursorPos: null,
1373-
};
1374-
modelCache[module.id].model = model;
1375-
modelCache[module.id].lib = lib;
1366+
return model;
1367+
}
13761368

1377-
return model;
1369+
return undefined;
13781370
};
13791371

13801372
getModelById = async (id: string) => {

packages/app/src/app/pages/Live/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as React from 'react';
22
import { inject, observer } from 'mobx-react';
33
import { Link } from 'react-router-dom';
44
import { DragDropContext } from 'react-dnd';
5-
import HTML5Backend from 'react-dnd-html5-backend';
65

76
import Navigation from 'app/pages/common/Navigation';
87
import Fullscreen from 'common/components/flex/Fullscreen';
@@ -15,6 +14,7 @@ import Skeleton from 'app/components/Skeleton';
1514
import Padding from 'common/components/spacing/Padding';
1615
import SignInButton from 'app/pages/common/SignInButton';
1716

17+
import HTML5Backend from '../common/HTML5BackendWithFolderSupport';
1818
import Editor from '../Sandbox/Editor';
1919
import BlinkingDot from './BlinkingDot';
2020

packages/app/src/app/pages/Patron/PricingModal/PricingInfo/index.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ function PricingInfo() {
2626
<Feature feature="Private Sandboxes" free="No" supporter="Yes" />
2727
<Feature feature="Sandbox Limit" free="50" supporter="Unlimited" />
2828
<Feature
29-
disabled
3029
feature="Static File Hosting"
31-
free="10Mb"
32-
supporter="1Gb"
30+
free="20Mb"
31+
supporter="500Mb"
3332
/>
3433
</tbody>
3534
</table>

packages/app/src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/Entry/EditIcons/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function EditIcons({
4646
</Tooltip>
4747
)}
4848
{onUploadFile && (
49-
<Tooltip title="Upload File">
49+
<Tooltip title="Upload Files">
5050
<Icon onClick={handleClick(onUploadFile)}>
5151
<UploadFileIcon />
5252
</Icon>

packages/app/src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/Entry/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class Entry extends React.PureComponent {
105105
icon: AddDirectoryIcon,
106106
},
107107
onUploadFileClick && {
108-
title: 'Upload File',
108+
title: 'Upload Files',
109109
action: onUploadFileClick,
110110
icon: UploadFileIcon,
111111
},

packages/app/src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/index.js

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,38 @@ import { DropTarget } from 'react-dnd';
44
import { reaction } from 'mobx';
55
import Modal from 'app/components/Modal';
66
import Alert from 'app/components/Alert';
7+
import { NativeTypes } from 'react-dnd-html5-backend';
78

89
import validateTitle from './validateTitle';
910
import Entry from './Entry';
1011
import DirectoryChildren from './DirectoryChildren';
1112
import { EntryContainer, Overlay, Opener } from './elements';
1213

14+
const readDataURL = imageFile =>
15+
new Promise(resolve => {
16+
const reader = new FileReader();
17+
reader.onload = e => {
18+
resolve(e.target.result);
19+
};
20+
reader.readAsDataURL(imageFile);
21+
});
22+
23+
const getFiles = async files => {
24+
const returnedFiles = {};
25+
await Promise.all(
26+
Array.from(files)
27+
.filter(Boolean)
28+
.map(async file => {
29+
const dataURI = await readDataURL(file);
30+
returnedFiles[file.path || file.name] = {
31+
dataURI,
32+
type: file.type,
33+
};
34+
})
35+
);
36+
37+
return returnedFiles;
38+
};
1339
class DirectoryEntry extends React.Component {
1440
constructor(props) {
1541
super(props);
@@ -101,32 +127,19 @@ class DirectoryEntry extends React.Component {
101127
onUploadFileClick = () => {
102128
const fileSelector = document.createElement('input');
103129
fileSelector.setAttribute('type', 'file');
104-
fileSelector.onchange = event => {
105-
const file = event.target.files[0];
106-
if (!file) {
107-
return;
108-
}
130+
fileSelector.setAttribute('multiple', 'true');
131+
fileSelector.onchange = async event => {
132+
const files = await getFiles(event.target.files);
109133

110-
this.readImageFile(file, base64Image => {
111-
this.props.signals.files.fileUploaded({
112-
content: base64Image,
113-
name: file.name,
114-
directoryShortid: this.props.shortid,
115-
});
134+
this.props.signals.files.filesUploaded({
135+
files,
136+
directoryShortid: this.props.shortid,
116137
});
117138
};
118139

119140
fileSelector.click();
120141
};
121142

122-
readImageFile = (imageFile, callback) => {
123-
const reader = new FileReader();
124-
reader.onload = e => {
125-
callback(e.target.result);
126-
};
127-
reader.readAsDataURL(imageFile);
128-
};
129-
130143
renameDirectory = (directoryShortid, title) => {
131144
this.props.signals.files.directoryRenamed({ title, directoryShortid });
132145
};
@@ -325,8 +338,16 @@ const entryTarget = {
325338
if (!monitor.isOver({ shallow: true })) return;
326339

327340
const sourceItem = monitor.getItem();
341+
if (sourceItem.dirContent) {
342+
sourceItem.dirContent.then(async droppedFiles => {
343+
const files = await getFiles(droppedFiles);
328344

329-
if (sourceItem.directory) {
345+
props.signals.files.filesUploaded({
346+
files,
347+
directoryShortid: props.shortid,
348+
});
349+
});
350+
} else if (sourceItem.directory) {
330351
props.signals.files.directoryMovedToDirectory({
331352
shortid: sourceItem.shortid,
332353
directoryShortid: props.shortid,
@@ -362,5 +383,7 @@ function collectTarget(connectMonitor, monitor) {
362383
}
363384

364385
export default inject('signals', 'store')(
365-
DropTarget('ENTRY', entryTarget, collectTarget)(observer(DirectoryEntry))
386+
DropTarget(['ENTRY', NativeTypes.FILE], entryTarget, collectTarget)(
387+
observer(DirectoryEntry)
388+
)
366389
);

packages/app/src/app/pages/Sandbox/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as React from 'react';
22
import { inject, observer } from 'mobx-react';
33
import { Link } from 'react-router-dom';
44
import { DragDropContext } from 'react-dnd';
5-
import HTML5Backend from 'react-dnd-html5-backend';
65
import QuickActions from 'app/pages/Sandbox/QuickActions';
76

87
import Navigation from 'app/pages/common/Navigation';
@@ -13,6 +12,7 @@ import Padding from 'common/components/spacing/Padding';
1312
import Skeleton from 'app/components/Skeleton';
1413

1514
import Editor from './Editor';
15+
import HTML5Backend from '../common/HTML5BackendWithFolderSupport';
1616

1717
class SandboxPage extends React.Component {
1818
componentWillMount() {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { join } from 'path';
2+
import { getFileFromFileEntry, getListAsArray } from './utils';
3+
4+
const getEntryData = (entry, options, level, path) => {
5+
let promise;
6+
7+
if (entry.isDirectory) {
8+
promise = options.recursive
9+
? getFileList(entry, options, level + 1, join(path, entry.name)) // eslint-disable-line
10+
: Promise.resolve([]);
11+
} else {
12+
promise = getFileFromFileEntry(entry).then(file => {
13+
if (file) {
14+
// eslint-disable-next-line no-param-reassign
15+
file.path = join(path, file.name);
16+
}
17+
return file ? [file] : [];
18+
});
19+
}
20+
21+
return promise;
22+
};
23+
24+
/**
25+
* returns a flat list of files for root dir item
26+
* if recursive is true will get all files from sub folders
27+
*/
28+
const getFileList = (root, options, level = 0, path = root.name) =>
29+
root && level < options.bail && root.isDirectory && root.createReader
30+
? new Promise(resolve => {
31+
root.createReader().readEntries(
32+
entries =>
33+
Promise.all(
34+
entries.map(entry => getEntryData(entry, options, level, path))
35+
).then(results => resolve(getListAsArray(results))), // flatten the results
36+
() => resolve([])
37+
); // fail silently
38+
})
39+
: Promise.resolve([]);
40+
41+
export default getFileList;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import getFileList from './file-list';
2+
import {
3+
isItemFileEntry,
4+
getListAsArray,
5+
getAsEntry,
6+
initOptions,
7+
} from './utils';
8+
9+
/**
10+
* returns a Promise<Array<File>> of File objects for the provided item if it represents a directory
11+
* will attempt to retrieve all of its children files (optionally recursively)
12+
* @param item: DataTransferItem
13+
* @param options (optional)
14+
* {options.recursive} (default: false) - whether to recursively follow the dir structure
15+
* {options.bail} (default: 1000) - how many levels to follow recursively before bailing
16+
*/
17+
const getFiles = (item, options = {}) =>
18+
getFileList(getAsEntry(item), initOptions(options));
19+
20+
const getDataTransferItemFiles = (item, options) =>
21+
getFiles(item, options).then(files => {
22+
if (!files.length) {
23+
// perhaps its a regular file
24+
const file = item.getAsFile();
25+
// eslint-disable-next-line
26+
files = file ? [file] : files;
27+
}
28+
29+
return files;
30+
});
31+
32+
/**
33+
* returns a Promise<Array<File>> for the File objects found in the dataTransfer data of a drag&drop event
34+
* In case a directory is found, will attempt to retrieve all of its children files (optionally recursively)
35+
*
36+
* @param evt: DragEvent - containing dataTransfer
37+
* @param options (optional)
38+
* {options.recursive} (default: false) - whether to recursively follow the dir structure
39+
* {options.bail} (default: 1000) - how many levels to follow recursively before bailing
40+
*/
41+
const getFilesFromDragEvent = (evt, options = {}) => {
42+
// eslint-disable-next-line
43+
options = initOptions(options);
44+
45+
return new Promise(resolve => {
46+
if (evt.dataTransfer.items) {
47+
Promise.all(
48+
getListAsArray(evt.dataTransfer.items)
49+
.filter(item => isItemFileEntry(item))
50+
.map(item => getDataTransferItemFiles(item, options))
51+
).then(files => resolve(getListAsArray(files)));
52+
} else if (evt.dataTransfer.files) {
53+
resolve(getListAsArray(evt.dataTransfer.files)); // turn into regular array (instead of FileList)
54+
} else {
55+
resolve([]);
56+
}
57+
});
58+
};
59+
60+
export { getFiles, getFilesFromDragEvent };

0 commit comments

Comments
 (0)