From 7862fecbc990a580970aaeef9de5667da268cc9f Mon Sep 17 00:00:00 2001
From: rr- <rr-@sakuya.pl>
Date: Thu, 29 Sep 2016 21:36:59 +0200
Subject: [PATCH] client/posts: add upload cancelling

---
 client/css/post-upload.styl                   | 13 +++++
 client/html/post_upload.tpl                   |  1 +
 client/js/api.js                              | 49 +++++++++++++------
 .../js/controllers/post_upload_controller.js  | 13 ++++-
 client/js/models/post.js                      | 10 +++-
 client/js/views/post_upload_view.js           | 22 +++++++--
 6 files changed, 87 insertions(+), 21 deletions(-)

diff --git a/client/css/post-upload.styl b/client/css/post-upload.styl
index 30f6e22..7bf9fbe 100644
--- a/client/css/post-upload.styl
+++ b/client/css/post-upload.styl
@@ -1,5 +1,7 @@
 @import colors
 
+$cancel-button-color = tomato
+
 #post-upload
     form
         width: 100%
@@ -19,6 +21,17 @@
 
     input[type=submit]
         margin-top: 1em
+        &[disabled]
+            display: none
+
+    .cancel
+        margin-top: 1em
+        background: $cancel-button-color
+        border-color: $cancel-button-color
+        &[disabled]
+            display: none
+        &:focus
+            border: 2px solid $text-color
 
     .messages
         margin-top: 1em
diff --git a/client/html/post_upload.tpl b/client/html/post_upload.tpl
index faf5f86..1dcfba4 100644
--- a/client/html/post_upload.tpl
+++ b/client/html/post_upload.tpl
@@ -3,6 +3,7 @@
         <div class='dropper-container'></div>
 
         <input type='submit' value='Upload all' class='submit'/>
+        <input type='button' value='Cancel' class='cancel'/>
 
         <div class='messages'></div>
 
diff --git a/client/js/api.js b/client/js/api.js
index 535b880..fe0f4ed 100644
--- a/client/js/api.js
+++ b/client/js/api.js
@@ -64,11 +64,13 @@ class Api extends events.EventTarget {
     _process(url, requestFactory, data, files, options) {
         options = options || {};
         const [fullUrl, query] = this._getFullUrl(url);
-        return new Promise((resolve, reject) => {
-            if (!options.noProgress) {
-                nprogress.start();
-            }
+
+        let abortFunction = null;
+
+        let promise = new Promise((resolve, reject) => {
             let req = requestFactory(fullUrl);
+
+            req.set('Accept', 'application/json');
             if (query) {
                 req.query(query);
             }
@@ -94,18 +96,35 @@ class Api extends events.EventTarget {
                     title: 'Authentication error',
                     description: 'Malformed credentials'});
             }
-            req.set('Accept', 'application/json')
-                .end((error, response) => {
-                    nprogress.done();
-                    if (error) {
-                        reject(response && response.body ? response.body : {
-                            title: 'Networking error',
-                            description: error.message});
-                    } else {
-                        resolve(response.body);
-                    }
-                });
+
+            if (!options.noProgress) {
+                nprogress.start();
+            }
+
+            abortFunction = () => {
+                req.abort();  // does *NOT* call the callback passed in .end()
+                nprogress.done();
+                reject({
+                    title: 'Cancelled',
+                    description:
+                        'The request was aborted due to user cancel.'});
+            };
+
+            req.end((error, response) => {
+                nprogress.done();
+                if (error) {
+                    reject(response && response.body ? response.body : {
+                        title: 'Networking error',
+                        description: error.message});
+                } else {
+                    resolve(response.body);
+                }
+            });
         });
+
+        promise.abort = () => abortFunction();
+
+        return promise;
     }
 
     hasPrivilege(lookup) {
diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js
index c9da651..4defd5e 100644
--- a/client/js/controllers/post_upload_controller.js
+++ b/client/js/controllers/post_upload_controller.js
@@ -10,6 +10,8 @@ const EmptyView = require('../views/empty_view.js');
 
 class PostUploadController {
     constructor() {
+        this._lastPromise = null;
+
         if (!api.hasPrivilege('posts:create')) {
             this._view = new EmptyView();
             this._view.showError('You don\'t have privileges to upload posts.');
@@ -23,6 +25,7 @@ class PostUploadController {
         });
         this._view.addEventListener('change', e => this._evtChange(e));
         this._view.addEventListener('submit', e => this._evtSubmit(e));
+        this._view.addEventListener('cancel', e => this._evtCancel(e));
     }
 
     _evtChange(e) {
@@ -34,6 +37,12 @@ class PostUploadController {
         this._view.clearMessages();
     }
 
+    _evtCancel(e) {
+        if (this._lastPromise) {
+            this._lastPromise.abort();
+        }
+    }
+
     _evtSubmit(e) {
         this._view.disableForm();
         this._view.clearMessages();
@@ -48,7 +57,9 @@ class PostUploadController {
                     } else {
                         post.newContent = uploadable.file;
                     }
-                    return post.save(uploadable.anonymous)
+                    let modelPromise = post.save(uploadable.anonymous);
+                    this._lastPromise = modelPromise;
+                    return modelPromise
                         .then(() => {
                             this._view.removeUploadable(uploadable);
                             return Promise.resolve();
diff --git a/client/js/models/post.js b/client/js/models/post.js
index 7e32143..f5f8f00 100644
--- a/client/js/models/post.js
+++ b/client/js/models/post.js
@@ -135,11 +135,11 @@ class Post extends events.EventTarget {
             files.thumbnail = this._newThumbnail;
         }
 
-        let promise = this._id ?
+        let apiPromise = this._id ?
             api.put('/post/' + this._id, detail, files) :
             api.post('/posts', detail, files);
 
-        return promise.then(response => {
+        let returnedPromise = apiPromise.then(response => {
             this._updateFromResponse(response);
             this.dispatchEvent(
                 new CustomEvent('change', {detail: {post: this}}));
@@ -159,6 +159,12 @@ class Post extends events.EventTarget {
             }
             return Promise.reject(response.description);
         });
+
+        returnedPromise.abort = () => {
+            apiPromise.abort();
+        };
+
+        return returnedPromise;
     }
 
     feature() {
diff --git a/client/js/views/post_upload_view.js b/client/js/views/post_upload_view.js
index c40a626..98b8021 100644
--- a/client/js/views/post_upload_view.js
+++ b/client/js/views/post_upload_view.js
@@ -133,10 +133,13 @@ class PostUploadView extends events.EventTarget {
         super();
         this._ctx = ctx;
         this._hostNode = document.getElementById('content-holder');
-        this._enabled = true;
+
         views.replaceContent(this._hostNode, template());
         views.syncScrollPosition();
 
+        this._enabled = true;
+        this._cancelButtonNode.disabled = true;
+
         this._uploadables = new Map();
         this._contentFileDropper = new FileDropperControl(
             this._contentInputNode,
@@ -150,18 +153,22 @@ class PostUploadView extends events.EventTarget {
         this._contentFileDropper.addEventListener(
             'urladd', e => this._evtUrlsAdded(e));
 
+        this._cancelButtonNode.addEventListener(
+            'click', e => this._evtCancelButtonClick(e));
         this._formNode.addEventListener('submit', e => this._evtFormSubmit(e));
         this._formNode.classList.add('inactive');
     }
 
     enableForm() {
-        this._enabled = true;
         views.enableForm(this._formNode);
+        this._cancelButtonNode.disabled = true;
+        this._enabled = true;
     }
 
     disableForm() {
-        this._enabled = false;
         views.disableForm(this._formNode);
+        this._cancelButtonNode.disabled = false;
+        this._enabled = false;
     }
 
     clearMessages() {
@@ -226,6 +233,11 @@ class PostUploadView extends events.EventTarget {
         this.addUploadables(e.detail.urls.map(url => new Url(url)));
     }
 
+    _evtCancelButtonClick(e) {
+        e.preventDefault();
+        this._emit('cancel');
+    }
+
     _evtFormSubmit(e) {
         e.preventDefault();
         this._emit('submit');
@@ -342,6 +354,10 @@ class PostUploadView extends events.EventTarget {
         return this._hostNode.querySelector('form [type=submit]');
     }
 
+    get _cancelButtonNode() {
+        return this._hostNode.querySelector('form .cancel');
+    }
+
     get _contentInputNode() {
         return this._formNode.querySelector('.dropper-container');
     }