MarkTraceur has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/259320

Change subject: [WIP] Resume stashed uploads
......................................................................

[WIP] Resume stashed uploads

No, really. Resume stashed uploads.

You get a little popup that, on click, pulls up a dialog, which lets you
choose uploads to resume. Works mixed with normal uploads. Not tested with
Flickr yet. Needs UI polish.

Bug: T85561
Bug: T120711
Change-Id: I8f7196151284cac26ee674807536c316e44b4870
---
M UploadWizardHooks.php
M i18n/en.json
M i18n/qqq.json
M resources/controller/uw.controller.Step.js
M resources/controller/uw.controller.Upload.js
M resources/mw.UploadWizardUpload.js
M resources/ui/steps/uw.ui.Upload.js
7 files changed, 368 insertions(+), 15 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/UploadWizard 
refs/changes/20/259320/1

diff --git a/UploadWizardHooks.php b/UploadWizardHooks.php
index 65bf5fd..95577c9 100644
--- a/UploadWizardHooks.php
+++ b/UploadWizardHooks.php
@@ -706,6 +706,11 @@
                                'mwe-upwiz-file-all-ok',
                                'mwe-upwiz-file-some-failed',
                                'mwe-upwiz-file-all-failed',
+                               'mwe-upwiz-unfinished-uploads-warning',
+                               'mwe-upwiz-cancel-resume',
+                               'mwe-upwiz-resume-dialog-title',
+                               'mwe-upwiz-resume-uploads',
+                               'mwe-upwiz-resume-dialog-choose',
                        ),
                ),
        );
diff --git a/i18n/en.json b/i18n/en.json
index 8a7c454..7f7ad75 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -39,6 +39,11 @@
        "mwe-upwiz-add-file-flickr-n": "Add more images from Flickr",
        "mwe-upwiz-add-flickr-or": "Or",
        "mwe-upwiz-add-flickr": "Get from Flickr",
+       "mwe-upwiz-unfinished-uploads-warning": "You have {{PLURAL:$1|an|$1}} 
unfinished {{PLURAL:$1|upload|uploads}}. Click here to finish uploading 
{{PLURAL:$1|it|them}}.",
+       "mwe-upwiz-cancel-resume": "Cancel",
+       "mwe-upwiz-resume-dialog-title": "Resume uploads",
+       "mwe-upwiz-resume-uploads": "Resume",
+       "mwe-upwiz-resume-dialog-choose": "Select which files you would like to 
continue uploading.",
        "mwe-upwiz-flickr-input-placeholder": "Flickr URL",
        "mwe-upwiz-select-flickr": "Upload selected images",
        "mwe-upwiz-flickr-disclaimer1": "This form will load content hosted by 
flickr.com and subject to the\nFlickr [https://www.flickr.com/help/terms/ terms 
of use] and [https://www.flickr.com/help/privacy-policy/ privacy policy].",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index c05302c..14e8f80 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -64,6 +64,11 @@
        "mwe-upwiz-add-flickr-or": "Connecting word in upload wizard between 
the following action elements:\n* {{msg-mw|mwe-upwiz-add-file-0-free}}\n* 
{{msg-mw|mwe-upwiz-add-file-flickr}}\n{{Identical|Or}}",
        "mwe-upwiz-add-flickr": "Label of the button which confirms the URL, 
entered by the user, of a photo or photo set from Flickr.",
        "mwe-upwiz-flickr-input-placeholder": "Used as placeholder for the 
input box.\n\nFollowed by the Submit button which is labeled 
{{msg-mw|Mwe-upwiz-add-flickr}}.",
+       "mwe-upwiz-unfinished-uploads-warning": "Warns the user that they have 
uploaded files, but have not finished the upload process. $1 is the number of 
files that are unfinished.",
+       "mwe-upwiz-cancel-resume": "{{Identical|Cancel}}",
+       "mwe-upwiz-resume-dialog-title": "Title of a dialog where a user may 
choose to resume unfinished uploads.",
+       "mwe-upwiz-resume-uploads": "Button used to resume unfinished uploads 
after choosing several of them.",
+       "mwe-upwiz-resume-dialog-choose": "Header for a form where the user 
chooses unfinished uploads to resume.",
        "mwe-upwiz-select-flickr": "Used as label for the submit button.",
        "mwe-upwiz-flickr-disclaimer1": "This is a legal disclaimer to let the 
user know that the Flickr terms of use and privacy policy apply to all content 
loaded from Flickr. Please try to keep the wording similar to the approved 
version in English.",
        "mwe-upwiz-flickr-disclaimer2": "This is a legal disclaimer to let the 
user know that their IP address will be sent to Flickr when they submit the 
form. Please try to keep the wording similar to the approved version in 
English.",
diff --git a/resources/controller/uw.controller.Step.js 
b/resources/controller/uw.controller.Step.js
index 3c0758d..9dda4d9 100644
--- a/resources/controller/uw.controller.Step.js
+++ b/resources/controller/uw.controller.Step.js
@@ -256,6 +256,7 @@
                        if ( upload === undefined ) {
                                return;
                        }
+
                        if ( upload.state === 'error' ) {
                                errorCount++;
                        } else if ( upload.state === desiredState ) {
@@ -415,7 +416,7 @@
         */
        uw.controller.Step.prototype.removeEmptyUploads = function () {
                this.removeMatchingUploads( function ( upload ) {
-                       return !upload || ( !upload.file && !upload.fromURL );
+                       return !upload || ( !upload.file && !upload.fromURL && 
!upload.fileKey );
                } );
        };
 
diff --git a/resources/controller/uw.controller.Upload.js 
b/resources/controller/uw.controller.Upload.js
index a829684..3b03b99 100644
--- a/resources/controller/uw.controller.Upload.js
+++ b/resources/controller/uw.controller.Upload.js
@@ -29,8 +29,9 @@
        uw.controller.Upload = function UWControllerUpload( api, config ) {
                uw.controller.Step.call(
                        this,
-                       new uw.ui.Upload( config )
+                       new uw.ui.Upload( config, api )
                                .connect( this, {
+                                       'resume-these': 'resumeChosenUploads',
                                        retry: 'retry',
                                        'flickr-ui-init': [ 'emit', 
'flickr-ui-init' ]
                                } ),
@@ -44,6 +45,8 @@
        };
 
        OO.inheritClass( uw.controller.Upload, uw.controller.Step );
+
+       uw.controller.Upload.prototype.filesDiv = '#mwe-upwiz-filelist';
 
        /**
         * Updates the upload step data when a file is added or removed.
@@ -59,6 +62,64 @@
                fewerThanMax = this.uploads.length < max;
 
                this.ui.updateFileCounts( haveUploads, fewerThanMax );
+       };
+
+       /**
+        * Checks for unfinished uploads in the stash from previous
+        * sessions - cached, so only checks once per session.
+        * 
+        * @return {jQuery.Promise}
+        */
+       uw.controller.Upload.prototype.checkForUnfinishedUploads = function () {
+               var step = this;
+
+               if ( !this.unfinishedPromise ) {
+                       this.unfinishedPromise = this.api.get( {
+                               action: 'query',
+                               list: 'mystashedimages',
+                               msiprop: [ 'size', 'type' ]
+                       } ).then( function ( data ) {
+                               if ( data && data.query ) {
+                                       return data.query.mystashedimages;
+                               }
+
+                               return [];
+                       } ).then( function ( images ) {
+                               var unfinished = [];
+
+                               $.each( images, function ( i, image ) {
+                                       if ( image.status && image.status === 
'finished' ) {
+                                               unfinished.push( image );
+                                       }
+                               } );
+
+                               return unfinished;
+                       } ).then( function ( unfinished ) {
+                               if ( unfinished.length > 0 ) {
+                                       step.warnAboutUnfinishedUploads( 
unfinished );
+                               }
+                       } );
+               }
+
+               return this.unfinishedPromise;
+       };
+
+       /**
+        * Warns the user that they have unfinished uploads and
+        * offers them an option to resume.
+        *
+        * @param {Object[]} images Partial API result, see 
#checkForUnfinishedUploads
+        */
+       uw.controller.Upload.prototype.warnAboutUnfinishedUploads = function ( 
images ) {
+               var step = this;
+
+               this.ui.on( 'resume-stashed-uploads', function ( images ) {
+                       $.each( images, function ( i, image ) {
+                               step.newUploadFromStash( image );
+                       } );
+               } );
+
+               this.ui.showUnfinishedWarning( images );
        };
 
        /**
@@ -83,6 +144,8 @@
                uw.controller.Step.prototype.moveTo.call( this );
                this.progressBar = undefined;
                this.resetUploads();
+
+               this.checkForUnfinishedUploads();
        };
 
        uw.controller.Upload.prototype.moveFrom = function () {
@@ -216,7 +279,7 @@
                        return false;
                }
 
-               upload = new mw.UploadWizardUpload( this.api, 
'#mwe-upwiz-filelist' )
+               upload = new mw.UploadWizardUpload( this.api, this.filesDiv )
                        .on( 'file-changed', function ( upload, files ) {
                                var duplicate = false,
                                        totalFiles = files.length + 
step.uploads.length,
@@ -257,6 +320,12 @@
                                        // NOTE: By running newUpload we will 
end up calling checkfile() again.
                                        var newUpload = step.newUpload();
 
+                                       // Other addition methods may have 
messed up the nextUpload
+                                       // stuff in the meantime.
+                                       while ( lastUpload.nextUpload !== null 
) {
+                                               lastUpload = 
lastUpload.nextUpload;
+                                       }
+
                                        lastUpload.setNextUpload( newUpload );
 
                                        if ( toobig ) {
@@ -291,11 +360,64 @@
                // we explicitly move the file input to cover the upload button
                upload.ui.moveFileInputToCover( '#mwe-upwiz-add-file', 'poll' );
 
+
+               this.handleNewUpload( upload );
+
+               this.waitingForUpload = upload;
+
+               return upload;
+       };
+
+       /**
+        * Resume uploads chosen in the UI.
+        *
+        * @param {Object[]} chosen
+        */
+       uw.controller.Upload.prototype.resumeChosenUploads = function ( chosen 
) {
+               var step = this;
+
+               $.each( chosen, function ( i, choice ) {
+                       step.newUploadFromStash( choice );
+               } );
+       };
+
+       /**
+        * Add an upload from the stash.
+        *
+        * @param {Object} image API result from mystashedimages, see 
#checkForUnfinishedUploads
+        */
+       uw.controller.Upload.prototype.newUploadFromStash = function ( image ) {
+               var upload, length = this.uploads.length;
+
+               if ( length >= this.config.maxUploads ) {
+                       return false;
+               }
+
+               upload = mw.UploadWizardUpload.newFromStash( this.api, 
this.filesDiv, image );
+
+               this.handleNewUpload( upload );
+               this.setUploadFilled( upload );
+
+               if ( length > 0 ) {
+                       // We had filled uploads before, add this one to the
+                       // chain.
+                       this.uploads[ length - 1 ].setNextUpload( upload );
+               } else {
+                       // This is the first upload, but the empty one is stored
+                       this.waitingForUpload.setNextUpload( upload );
+               }
+       };
+
+       /**
+        * Helper method for adding event handlers for uploads created
+        * from different sources.
+        *
+        * @param {mw.UploadWizardUpload} upload-
+        */
+       uw.controller.Upload.prototype.handleNewUpload = function ( upload ) {
                upload.connect( this, {
                        'remove-upload': [ 'removeUpload', upload ]
                } );
-
-               return upload;
        };
 
        /**
diff --git a/resources/mw.UploadWizardUpload.js 
b/resources/mw.UploadWizardUpload.js
index e333090..1d58d89 100644
--- a/resources/mw.UploadWizardUpload.js
+++ b/resources/mw.UploadWizardUpload.js
@@ -57,18 +57,62 @@
                this.ui = new mw.UploadWizardUploadInterface( this, filesDiv )
                        .connect( this, {
                                'file-changed': [ 'emit', 'file-changed', 
upload ],
-                               'filename-accepted': [ 'emit', 
'filename-accepted' ]
-                       } )
-
-                       .on( 'upload-filled', function () {
-                               upload.details = new mw.UploadWizardDetails( 
upload, $( '#mwe-upwiz-macro-files' ) );
-
-                               upload.emit( 'filled' );
+                               'filename-accepted': [ 'emit', 
'filename-accepted' ],
+                               'upload-filled': [ 'emit', 'filled' ]
                        } );
        };
 
        OO.mixinClass( mw.UploadWizardUpload, OO.EventEmitter );
 
+       /**
+        * Create a new upload from an upload already stashed.
+        *
+        * @static
+        * @param {mw.Api} api
+        * @param {Object} image From API result for mystashedimages.
+        */
+       mw.UploadWizardUpload.newFromStash = function ( api, filesDiv, image ) {
+               var upload = new mw.UploadWizardUpload( api, filesDiv );
+
+               // Set the state to stashed, no upload to perform
+               upload.state = 'stashed';
+
+               // Get the stashed image info...
+               api.get( {
+                       action: 'query',
+                       prop: 'stashedimageinfo',
+                       siifilekey: image.filekey,
+                       siiurlwidth: 200,
+                       siiurlheight: 200
+               } ).then( function ( data ) {
+                       if ( data && data.query && data.query.stashimageinfo ) {
+                               $.each( data.query.stashimageinfo, function ( 
i, info ) {
+                                       upload.extractImageInfo( info );
+                                       upload.ui.showStashed();
+                                       // We shouldn't get more than one, but 
make sure.
+                                       return false;
+                               } );
+                       }
+               } );
+
+               // Set details we already have...
+               upload.fileKey = image.filekey;
+               upload.mimetype = image.mimetype;
+
+               // Fake it 'til you make it
+               upload.filename = image.filekey;
+               upload.setTitle( image.filekey );
+
+               // Tell the interface to get image info and such.
+               upload.fileChangedOk();
+
+               // Tell the step controller to do its stuff
+               upload.emit( 'filled' );
+               upload.emit( 'success' );
+
+               return upload;
+       };
+
        // Upload handler
        mw.UploadWizardUpload.prototype.uploadHandler = null;
 
diff --git a/resources/ui/steps/uw.ui.Upload.js 
b/resources/ui/steps/uw.ui.Upload.js
index 0aa6cb2..b6e446c 100644
--- a/resources/ui/steps/uw.ui.Upload.js
+++ b/resources/ui/steps/uw.ui.Upload.js
@@ -17,17 +17,142 @@
 
 ( function ( mw, $, uw, OO ) {
        /**
+        * Button with popup information.
+        */
+       function PopupButtonWidget( config ) {
+               // Parent constructor
+               PopupButtonWidget.parent.call( this, config );
+
+               // Mixin constructors
+               OO.ui.mixin.PopupElement.call( this, config );
+
+               // Initialization
+               this.$element
+                       .addClass( 'oo-ui-popupButtonWidget' )
+                       .attr( 'aria-haspopup', 'true' )
+                       .append( this.popup.$element );
+       }
+       OO.inheritClass( PopupButtonWidget, OO.ui.ButtonWidget );
+       OO.mixinClass( PopupButtonWidget, OO.ui.mixin.PopupElement );
+
+       function ResumeDialog( config ) {
+               ResumeDialog.parent.call( this, config );
+
+               this.choices = config.choices;
+               this.api = config.api;
+               this.thumbnailWidth = config.thumbnailWidth;
+               this.thumbnailMaxHeight = config.thumbnailMaxHeight;
+       }
+       OO.inheritClass( ResumeDialog, OO.ui.ProcessDialog );
+       ResumeDialog.static.title = mw.message( 'mwe-upwiz-resume-dialog-title' 
).text();
+       ResumeDialog.static.actions = [
+               { action: 'resume', modes: 'choose', label: mw.message( 
'mwe-upwiz-resume-uploads' ).text(), flags: [ 'primary', 'constructive' ] },
+               { modes: 'choose', label: mw.message( 'mwe-upwiz-cancel-resume' 
).text(), flags: 'safe' }
+       ];
+
+       ResumeDialog.prototype.initialize = function () {
+               var dialog = this;
+
+               ResumeDialog.parent.prototype.initialize.apply( this, arguments 
);
+
+               this.chooseFieldsetLayout = new OO.ui.FieldsetLayout( {
+                       label: mw.message( 'mwe-upwiz-resume-dialog-choose' 
).text()
+               } );
+
+               this.checkboxes = [];
+
+               $.each( this.choices, function ( i, choice ) {
+                       var check = new OO.ui.CheckboxInputWidget( {
+                                       value: i,
+                                       selected: false
+                               } ),
+
+                               field = new OO.ui.FieldLayout( check, {
+                                       label: choice.filekey
+                               } );
+
+                       dialog.checkboxes.push( check );
+
+                       dialog.chooseFieldsetLayout.addItems( [ field ] );
+
+                       dialog.api.get( {
+                               action: 'query',
+                               prop: 'stashimageinfo',
+                               siifilekey: choice.filekey,
+                               siiurlwidth: dialog.thumbnailWidth,
+                               siiurlheight: dialog.thumbnailMaxHeight
+                       } ).then( function ( data ) {
+                               if ( data && data.query && 
data.query.stashimageinfo ) {
+                                       $.each( data.query.stashimageinfo, 
function ( i, image ) {
+                                               field.setLabel( $( '<img>' 
).attr( 'src', image.thumburl ) );
+
+                                               choice.thumburl = 
image.thumburl;
+
+                                               // We only need the first one...
+                                               return false;
+                                       } );
+                               }
+                       } );
+               } );
+
+               this.choosePanel = new OO.ui.PanelLayout( { padded: true, 
expanded: false } );
+               this.choosePanel.$element.append(
+                       this.chooseFieldsetLayout.$element
+               );
+
+               this.stackLayout = new OO.ui.StackLayout( {
+                       items: [ this.choosePanel ]
+               } );
+
+               this.$body.append( this.stackLayout.$element );
+       };
+
+       ResumeDialog.prototype.getSetupProcess = function ( data ) {
+               return ResumeDialog.parent.prototype.getSetupProcess.call( 
this, data )
+                       .next( function () {
+                               this.actions.setMode( 'choose' );
+                       }, this );
+       };
+
+       ResumeDialog.prototype.getActionProcess = function ( action ) {
+               var dialog = this;
+
+               if ( action === 'resume' ) {
+                       return new OO.ui.Process( function () {
+                               dialog.emit( 'resume-these', 
dialog.getChosenUploads() );
+                               dialog.close();
+                       } );
+               }
+       };
+
+       ResumeDialog.prototype.getChosenUploads = function () {
+               return this.checkboxes.map( function ( check, i ) {
+                       if ( check.isSelected() ) {
+                               return this.choices[ i ];
+                       }
+               }, this ).filter( function ( choice ) {
+                       return !!choice;
+               } );
+       };
+
+       ResumeDialog.prototype.getBodyHeight = function () {
+               return this.choosePanel.$element.outerHeight( true );
+       };
+
+       /**
         * Represents the UI for the wizard's Upload step.
         *
         * @class uw.ui.Upload
         * @extends uw.ui.Step
         * @constructor
         * @param {Object} config UploadWizard config object.
+        * @param {mw.Api} api API object for fetching thumbnails
         */
-       uw.ui.Upload = function UWUIUpload( config ) {
+       uw.ui.Upload = function UWUIUpload( config, api ) {
                var upload = this;
 
                this.config = config;
+               this.api = api;
 
                uw.ui.Step.call(
                        this,
@@ -76,10 +201,15 @@
                                )
                );
 
-               this.addFile = new OO.ui.ButtonWidget( {
+               this.addFile = new PopupButtonWidget( {
                        id: 'mwe-upwiz-add-file',
                        label: mw.message( 'mwe-upwiz-add-file-0-free' ).text(),
-                       flags: [ 'constructive', 'primary' ]
+                       flags: [ 'constructive', 'primary' ],
+
+                       popup: {
+                               autoClose: true,
+                               padded: true
+                       }
                } );
 
                this.$addFileContainer.prepend( this.addFile.$element );
@@ -182,6 +312,47 @@
        OO.inheritClass( uw.ui.Upload, uw.ui.Step );
 
        /**
+        * Shows a popup warning that warns the user about stashed files that
+        * they can resume.
+        *
+        * @param {Object[]} unfinished API results describing the stashed 
files.
+        */
+       uw.ui.Upload.prototype.showUnfinishedWarning = function ( unfinished ) {
+               var ui = this;
+
+               this.addFile.popup.$body.append(
+                       $( '<p>' ).msg( 'mwe-upwiz-unfinished-uploads-warning', 
unfinished.length )
+               );
+
+               this.addFile.popup.toggle( true );
+
+               this.addFile.popup.$body.on( 'click', function () {
+                       ui.addFile.popup.toggle( false );
+                       ui.showUnfinishedDialog( unfinished );
+               } );
+       };
+
+       uw.ui.Upload.prototype.showUnfinishedDialog = function ( unfinished ) {
+               this.unfinishedDialog = new ResumeDialog( {
+                       size: 'medium',
+                       choices: unfinished,
+                       thumbnailWidth: this.config.thumbnailWidth,
+                       thumbnailMaxHeight: this.config.thumbnailMaxHeight,
+                       api: this.api
+               } );
+
+               this.unfinishedDialog.connect( this, {
+                       'resume-these': [ 'emit', 'resume-these' ]
+               } );
+
+               this.windowManager = new OO.ui.WindowManager();
+               $( 'body' ).append( this.windowManager.$element );
+
+               this.windowManager.addWindows( [ this.unfinishedDialog ] );
+               this.windowManager.openWindow( this.unfinishedDialog );
+       };
+
+       /**
         * Updates the interface based on the number of uploads.
         *
         * @param {boolean} haveUploads Whether there are any uploads at all.

-- 
To view, visit https://gerrit.wikimedia.org/r/259320
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I8f7196151284cac26ee674807536c316e44b4870
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/UploadWizard
Gerrit-Branch: master
Gerrit-Owner: MarkTraceur <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to