Explorar o código

Merge pull request #2709 from blink1073/file-drag

Add native file drop
Afshin Darian %!s(int64=7) %!d(string=hai) anos
pai
achega
4184b62642

+ 20 - 9
packages/docmanager/src/dialogs.ts

@@ -86,21 +86,32 @@ function renameFile(manager: IDocumentManager, oldPath: string, newPath: string)
     if (error.message.indexOf('409') === -1) {
       throw error;
     }
-    let options = {
-      title: 'Overwrite file?',
-      body: `"${newPath}" already exists, overwrite?`,
-      buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'OVERWRITE' })]
-    };
-    return showDialog(options).then(result => {
-      if (!result.button.accept) {
-        return Promise.resolve(null);
+    return shouldOverwrite(newPath).then(value => {
+      if (value) {
+        return manager.overwrite(oldPath, newPath);
       }
-      return manager.overwrite(oldPath, newPath);
+      return Promise.reject('File not renamed');
     });
   });
 }
 
 
+/**
+ * Ask the user whether to overwrite a file.
+ */
+export
+function shouldOverwrite(path: string): Promise<boolean> {
+  let options = {
+    title: 'Overwrite file?',
+    body: `"${path}" already exists, overwrite?`,
+    buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'OVERWRITE' })]
+  };
+  return showDialog(options).then(result => {
+    return Promise.resolve(result.button.accept);
+  });
+}
+
+
 /**
  * An error message dialog to upon document manager errors.
  */

+ 7 - 24
packages/filebrowser/src/crumbs.ts

@@ -22,13 +22,17 @@ import {
 } from '@phosphor/widgets';
 
 import {
-  Dialog, DOMUtils, showDialog
+  DOMUtils
 } from '@jupyterlab/apputils';
 
 import {
   PathExt
 } from '@jupyterlab/coreutils';
 
+import {
+  renameFile
+} from '@jupyterlab/docmanager';
+
 import {
   FileBrowserModel
 } from './model';
@@ -270,6 +274,7 @@ class BreadCrumbs extends Widget {
 
     const path = BREAD_CRUMB_PATHS[index];
     const model = this._model;
+    const manager = model.manager;
 
     // Move all of the items.
     let promises: Promise<any>[] = [];
@@ -277,29 +282,7 @@ class BreadCrumbs extends Widget {
     for (let oldPath of oldPaths) {
       let name = PathExt.basename(oldPath);
       let newPath = PathExt.join(path, name);
-      promises.push(model.manager.rename(oldPath, newPath).catch(error => {
-        if (error.xhr) {
-          error.message = `${error.xhr.status}: error.statusText`;
-        }
-        if (error.message.indexOf('409') !== -1) {
-          let overwrite = Dialog.warnButton({ label: 'OVERWRITE' });
-          let options = {
-            title: 'Overwrite file?',
-            body: `"${newPath}" already exists, overwrite?`,
-            buttons: [Dialog.cancelButton(), overwrite]
-          };
-          return showDialog(options).then(result => {
-            if (!model.isDisposed && result.button.accept) {
-              return model.manager.deleteFile(newPath).then(() => {
-                if (!model.isDisposed) {
-                  return model.manager.rename(oldPath, newPath);
-                }
-                return Promise.reject('Model is disposed') as Promise<any>;
-              });
-            }
-          });
-        }
-      }));
+      promises.push(renameFile(manager, oldPath, newPath));
     }
     Promise.all(promises).catch(err => {
       utils.showErrorMessage('Move Error', err);

+ 32 - 2
packages/filebrowser/src/listing.ts

@@ -618,6 +618,13 @@ class DirListing extends Widget {
     case 'contextmenu':
       this._evtContextMenu(event as MouseEvent);
       break;
+    case 'dragenter':
+    case 'dragover':
+      event.preventDefault();
+      break;
+    case 'drop':
+      this._evtNativeDrop(event as DragEvent);
+      break;
     case 'scroll':
       this._evtScroll(event as MouseEvent);
       break;
@@ -650,6 +657,9 @@ class DirListing extends Widget {
     node.addEventListener('click', this);
     node.addEventListener('dblclick', this);
     node.addEventListener('contextmenu', this);
+    content.addEventListener('dragenter', this);
+    content.addEventListener('dragover', this);
+    content.addEventListener('drop', this);
     content.addEventListener('scroll', this);
     content.addEventListener('p-dragenter', this);
     content.addEventListener('p-dragleave', this);
@@ -670,6 +680,9 @@ class DirListing extends Widget {
     node.removeEventListener('dblclick', this);
     node.removeEventListener('contextmenu', this);
     content.removeEventListener('scroll', this);
+    content.removeEventListener('dragover', this);
+    content.removeEventListener('dragover', this);
+    content.removeEventListener('drop', this);
     content.removeEventListener('p-dragenter', this);
     content.removeEventListener('p-dragleave', this);
     content.removeEventListener('p-dragover', this);
@@ -985,6 +998,19 @@ class DirListing extends Widget {
     }
   }
 
+  /**
+   * Handle the `drop` event for the widget.
+   */
+  private _evtNativeDrop(event: DragEvent): void {
+    let files = event.dataTransfer.files;
+    if (files.length === 0) {
+      return;
+    }
+    event.preventDefault();
+    for (let i = 0; i < files.length; i++) {
+      this._model.upload(files[i]);
+    }
+  }
 
   /**
    * Handle the `'p-dragenter'` event for the widget.
@@ -1298,7 +1324,9 @@ class DirListing extends Widget {
       const newPath = PathExt.join(this._model.path, newName);
       const promise = renameFile(manager, oldPath, newPath);
       return promise.catch(error => {
-        utils.showErrorMessage('Rename Error', error);
+        if (error !== 'File not renamed') {
+          utils.showErrorMessage('Rename Error', error);
+        }
         this._inRename = false;
         return original;
       }).then(() => {
@@ -1306,7 +1334,9 @@ class DirListing extends Widget {
           this._inRename = false;
           return Promise.reject('Disposed') as Promise<string>;
         }
-        this.selectItemByName(newName);
+        if (this._inRename) {
+          this.selectItemByName(newName);
+        }
         this._inRename = false;
         return newName;
       });

+ 17 - 19
packages/filebrowser/src/model.ts

@@ -6,7 +6,7 @@ import {
 } from '@jupyterlab/coreutils';
 
 import {
-  IDocumentManager
+  IDocumentManager, shouldOverwrite
 } from '@jupyterlab/docmanager';
 
 import {
@@ -14,7 +14,7 @@ import {
 } from '@jupyterlab/services';
 
 import {
-  ArrayIterator, each, IIterator, IterableOrArrayLike
+  ArrayIterator, each, find, IIterator, IterableOrArrayLike
 } from '@phosphor/algorithm';
 
 import {
@@ -223,7 +223,7 @@ class FileBrowserModel implements IDisposable {
   /**
    * Download a file.
    *
-   * @param - path - The path of the file to be downloaded.
+   * @param path - The path of the file to be downloaded.
    *
    * @returns A promise which resolves when the file has begun
    *   downloading.
@@ -280,15 +280,13 @@ class FileBrowserModel implements IDisposable {
    *
    * @param file - The `File` object to upload.
    *
-   * @param overwrite - Whether to overwrite an existing file.
-   *
    * @returns A promise containing the new file contents model.
    *
    * #### Notes
    * This will fail to upload files that are too big to be sent in one
    * request to the server.
    */
-  upload(file: File, overwrite?: boolean): Promise<Contents.IModel> {
+  upload(file: File): Promise<Contents.IModel> {
     // Skip large files with a warning.
     if (file.size > this._maxUploadSizeMb * 1024 * 1024) {
       let msg = `Cannot upload file (>${this._maxUploadSizeMb} MB) `;
@@ -297,20 +295,20 @@ class FileBrowserModel implements IDisposable {
       return Promise.reject<Contents.IModel>(new Error(msg));
     }
 
-    if (overwrite) {
-      return this._upload(file);
-    }
-
-    let path = this._model.path;
-    path = path ? path + '/' + file.name : file.name;
-    return this.manager.services.contents.get(path, {}).then(() => {
-      let msg = `"${file.name}" already exists`;
-      throw new Error(msg);
-    }, () => {
+    return this.refresh().then(() => {
       if (this.isDisposed) {
-        return Promise.reject('Disposed') as Promise<Contents.IModel>;
+        return Promise.resolve(false);
+      }
+      let item = find(this._items, i => i.name === file.name);
+      if (item) {
+        return shouldOverwrite(file.name);
+      }
+      return Promise.resolve(true);
+    }).then(value => {
+      if (value) {
+        return this._upload(file);
       }
-      return this._upload(file);
+      return Promise.reject('File not uploaded');
     });
   }
 
@@ -525,7 +523,7 @@ namespace Private {
     if (parts.length === 1) {
       return PathExt.resolve(root, path);
     } else {
-      let resolved = PathExt.resolve(parts[1], path)
+      let resolved = PathExt.resolve(parts[1], path);
       return parts[0] + ':' + resolved;
     }
   }

+ 2 - 33
packages/filebrowser/src/upload.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  Dialog, ToolbarButton, showDialog
+  ToolbarButton
 } from '@jupyterlab/apputils';
 
 import {
@@ -76,7 +76,7 @@ class Uploader extends ToolbarButton {
    */
   private _onInputChanged(): void {
     let files = Array.prototype.slice.call(this._input.files) as File[];
-    let pending = files.map(file => this._uploadFile(file));
+    let pending = files.map(file => this.model.upload(file));
     Promise.all(pending).catch(error => {
       utils.showErrorMessage('Upload Error', error);
     });
@@ -91,37 +91,6 @@ class Uploader extends ToolbarButton {
     this._input.value = '';
   }
 
-  /**
-   * Upload a file to the server.
-   */
-  private _uploadFile(file: File): Promise<any> {
-    return this.model.upload(file).catch(error => {
-      let exists = error.message.indexOf('already exists') !== -1;
-      if (exists) {
-        return this._uploadFileOverride(file);
-      }
-      throw error;
-    });
-  }
-
-  /**
-   * Upload a file to the server checking for override.
-   */
-  private _uploadFileOverride(file: File): Promise<any> {
-    let overwrite = Dialog.warnButton({ label: 'OVERWRITE' });
-    let options = {
-      title: 'Overwrite File?',
-      body: `"${file.name}" already exists, overwrite?`,
-      buttons: [Dialog.cancelButton(), overwrite]
-    };
-    return showDialog(options).then(result => {
-      if (this.isDisposed || result.button.accept) {
-        return Promise.resolve(void 0);
-      }
-      return this.model.upload(file, true);
-    });
-  }
-
   private _input = Private.createUploadInput();
 }
 

+ 27 - 19
test/src/filebrowser/model.spec.ts

@@ -4,7 +4,7 @@
 import expect = require('expect.js');
 
 import {
-  StateDB
+  StateDB, uuid
 } from '@jupyterlab/coreutils';
 
 import {
@@ -23,6 +23,10 @@ import {
   FileBrowserModel
 } from '@jupyterlab/filebrowser';
 
+import {
+  acceptDialog, dismissDialog
+} from '../utils';
+
 
 describe('filebrowser/model', () => {
 
@@ -293,47 +297,51 @@ describe('filebrowser/model', () => {
     describe('#upload()', () => {
 
       it('should upload a file object', (done) => {
-        let file = new File(['<p>Hello world!</p>'], 'hello.html',
+        let fname = uuid() + '.html';
+        let file = new File(['<p>Hello world!</p>'], fname,
                             { type: 'text/html' });
         model.upload(file).then(contents => {
-          expect(contents.name).to.be('hello.html');
+          expect(contents.name).to.be(fname);
           done();
         }).catch(done);
       });
 
-      it('should allow overwrite', (done) => {
-        let file = new File(['<p>Hello world!</p>'], 'hello2.html',
+      it('should overwrite', () => {
+        let fname = uuid() + '.html';
+        let file = new File(['<p>Hello world!</p>'], fname,
                             { type: 'text/html' });
-        model.upload(file).then(contents => {
-          expect(contents.name).to.be('hello2.html');
-          return model.upload(file, true);
+        return model.upload(file).then(contents => {
+          expect(contents.name).to.be(fname);
+          acceptDialog();
+          return model.upload(file);
         }).then(contents => {
-          expect(contents.name).to.be('hello2.html');
-          done();
-        }).catch(done);
+          expect(contents.name).to.be(fname);
+        });
       });
 
-      it('should fail without an overwrite if the file exists', (done) => {
-        let file = new File(['<p>Hello world!</p>'], 'hello2.html',
+      it('should not overwrite', () => {
+        let fname = uuid() + '.html';
+        let file = new File(['<p>Hello world!</p>'], fname,
                             { type: 'text/html' });
-        model.upload(file).then(contents => {
-          expect(contents.name).to.be('hello2.html');
+        return model.upload(file).then(contents => {
+          expect(contents.name).to.be(fname);
+          dismissDialog();
           return model.upload(file);
         }).catch(err => {
-          expect(err.message).to.be(`"${file.name}" already exists`);
-          done();
+          expect(err).to.be('File not uploaded');
         });
       });
 
       it('should emit the fileChanged signal', (done) => {
+        let fname = uuid() + '.html';
         model.fileChanged.connect((sender, args) => {
           expect(sender).to.be(model);
           expect(args.type).to.be('save');
           expect(args.oldValue).to.be(null);
-          expect(args.newValue.path).to.be('hello3.html');
+          expect(args.newValue.path).to.be(fname);
           done();
         });
-        let file = new File(['<p>Hello world!</p>'], 'hello3.html',
+        let file = new File(['<p>Hello world!</p>'], fname,
                             { type: 'text/html' });
         model.upload(file).catch(done);
       });