Finish stash uploads with upload dialog

Adds a 'filekey' option to the upload dialog's booklet layout
so it's possible to complete a stashed upload with the dialog,
even if the dialog didn't stash the file.

Proof of concept gadget/userscript for testing purposes:

https://phabricator.wikimedia.org/P2783

Or use this ImageTweaks patch to test:

I8f8421697a6d44ec47b66496ad9ada548c4a7d0b

Change-Id: I2cdea08a29cd481eb7fe5cbd83b9e4b6941a6380
This commit is contained in:
Mark Holmquist 2016-03-04 11:51:58 -06:00
parent 541b2e7e5b
commit 11a2791668
6 changed files with 438 additions and 22 deletions

View file

@ -1196,6 +1196,7 @@ return [
'mediawiki.user',
'mediawiki.Upload',
'mediawiki.jqueryMsg',
'mediawiki.widgets.StashedFileWidget'
],
'messages' => [
'upload-form-label-infoform-title',
@ -2237,7 +2238,19 @@ return [
'position' => 'top',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.widgets.StashedFileWidget' => [
'scripts' => [
'resources/src/mediawiki.widgets/mw.widgets.StashedFileWidget.js',
],
'skinStyles' => [
'default' => [
'resources/src/mediawiki.widgets/mw.widgets.StashedFileWidget.less',
],
],
'dependencies' => [
'oojs-ui-core',
],
],
/* es5-shim */
'es5-shim' => [
'scripts' => [

View file

@ -0,0 +1,158 @@
/*!
* MediaWiki Widgets - StashedFileWidget class.
*
* @copyright 2011-2016 MediaWiki Widgets Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function ( $, mw, OO ) {
/**
* Accepts a stashed file and displays the information for purposes of
* publishing the file at the behest of the user.
*
* Example use:
* var widget = new mw.widgets.StashedFileWidget( {
* filekey: '12r9e4rugeec.ddtmmp.1.jpg',
* } );
*
* widget.getValue(); // '12r9e4rugeec.ddtmmp.1.jpg'
* widget.setValue( '12r9epfbnskk.knfiy7.1.jpg' );
* widget.getValue(); // '12r9epfbnskk.knfiy7.1.jpg'
*
* Note that this widget will not finish an upload for you. Use mw.Upload
* and mw.Upload#setFilekey, then mw.Upload#finishStashUpload to accomplish
* that.
*
* @class mw.widgets.StashedFileWidget
* @extends OO.ui.Widget
*/
/**
* @constructor
* @param {Object} config Configuration options
* @cfg {string} filekey The filekey of the stashed file.
* @cfg {Object} [api] API to use for thumbnails.
*/
mw.widgets.StashedFileWidget = function MWWStashedFileWidget( config ) {
if ( !config.api ) {
config.api = new mw.Api();
}
// Parent constructor
mw.widgets.StashedFileWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.IconElement.call( this, config );
OO.ui.mixin.LabelElement.call( this, config );
OO.ui.mixin.PendingElement.call( this, config );
// Properties
this.api = config.api;
this.$info = $( '<span>' );
this.setValue( config.filekey );
this.$label.addClass( 'mw-widgets-stashedFileWidget-label' );
this.$info
.addClass( 'mw-widgets-stashedFileWidget-info' )
.append( this.$icon, this.$label );
this.$thumbnail = $( '<div>' ).addClass( 'mw-widgets-stashedFileWidget-thumbnail' );
this.setPendingElement( this.$thumbnail );
this.$thumbContain = $( '<div>' )
.addClass( 'mw-widgets-stashedFileWidget-thumbnail-container' )
.append( this.$thumbnail, this.$info );
this.$element
.addClass( 'mw-widgets-stashedFileWidget' )
.append( this.$thumbContain );
this.updateUI();
};
OO.inheritClass( mw.widgets.StashedFileWidget, OO.ui.Widget );
OO.mixinClass( mw.widgets.StashedFileWidget, OO.ui.mixin.IconElement );
OO.mixinClass( mw.widgets.StashedFileWidget, OO.ui.mixin.LabelElement );
OO.mixinClass( mw.widgets.StashedFileWidget, OO.ui.mixin.PendingElement );
/**
* Get the current filekey.
*
* @return {string|null}
*/
mw.widgets.StashedFileWidget.prototype.getValue = function () {
return this.filekey;
};
/**
* Set the filekey.
*
* @param {string|null} filekey
*/
mw.widgets.StashedFileWidget.prototype.setValue = function ( filekey ) {
if ( filekey !== this.filekey ) {
this.filekey = filekey;
this.updateUI();
this.emit( 'change', this.filekey );
}
};
mw.widgets.StashedFileWidget.prototype.updateUI = function () {
var $label, $filetype;
if ( this.filekey ) {
this.$element.removeClass( 'mw-widgets-stashedFileWidget-empty' );
$label = $( [] );
$filetype = $( '<span>' )
.addClass( 'mw-widgets-stashedFileWidget-fileType' );
$label = $label.add(
$( '<span>' )
.addClass( 'mw-widgets-stashedFileWidget-filekey' )
.text( this.filekey )
).add( $filetype );
this.setLabel( $label );
this.pushPending();
this.loadAndGetImageUrl().done( function ( url, mime ) {
this.$thumbnail.css( 'background-image', 'url( ' + url + ' )' );
if ( mime ) {
$filetype.text( mime );
this.setLabel( $label );
}
}.bind( this ) ).fail( function () {
this.$thumbnail.append(
new OO.ui.IconWidget( {
icon: 'attachment',
classes: [ 'mw-widgets-stashedFileWidget-noThumbnail-icon' ]
} ).$element
);
}.bind( this ) ).always( function () {
this.popPending();
}.bind( this ) );
} else {
this.$element.addClass( 'mw-widgets-stashedFileWidget-empty' );
this.setLabel( '' );
}
};
mw.widgets.StashedFileWidget.prototype.loadAndGetImageUrl = function () {
var filekey = this.filekey;
if ( filekey ) {
return this.api.get( {
action: 'query',
prop: 'stashimageinfo',
siifilekey: filekey,
siiprop: [ 'size', 'url', 'mime' ],
siiurlwidth: 220
} ).then( function ( data ) {
var sii = data.query.stashimageinfo[ 0 ];
return $.Deferred().resolve( sii.thumburl, sii.mime );
} );
}
return $.Deferred().reject( 'No filekey' );
};
}( jQuery, mediaWiki, OO ) );

View file

@ -0,0 +1,172 @@
.mw-widgets-stashedFileWidget {
display: inline-block;
vertical-align: middle;
width: 100%;
max-width: 50em;
margin-right: 0.5em;
&:last-child {
margin-right: 0;
}
&.oo-ui-iconElement .mw-widgets-stashedFileWidget-info .mw-widgets-stashedFileWidget-label {
left: 2.875em;
}
&.oo-ui-indicatorElement .mw-widgets-stashedFileWidget-info .mw-widgets-stashedFileWidget-label {
right: 4.4625em;
}
}
.mw-widgets-stashedFileWidget-info {
height: 2.4em;
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 2px;
width: 100%;
display: table-cell;
vertical-align: middle;
position: relative;
overflow: hidden;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
> .mw-widgets-stashedFileWidget-label {
line-height: 2.3em;
margin: 0;
overflow: hidden;
white-space: nowrap;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
text-overflow: ellipsis;
left: 0.5em;
right: 2.375em;
position: absolute;
top: 0;
bottom: 0;
> .mw-widgets-stashedFileWidget-fileName {
float: left;
}
> .mw-widgets-stashedFileWidget-fileType {
color: #888888;
float: right;
}
}
> .oo-ui-indicatorElement-indicator,
> .oo-ui-iconElement-icon {
position: absolute;
}
> .oo-ui-indicatorElement-indicator {
right: 0;
top: 0;
width: 0.9375em;
height: 2.3em;
margin-right: 0.775em;
}
> .oo-ui-iconElement-icon {
top: 0;
width: 1.875em;
height: 2.3em;
margin-left: 0.5em;
left: 0;
}
&.oo-ui-widget-disabled {
.mw-widgets-stashedFileWidget-info {
color: #cccccc;
text-shadow: 0 1px 1px #ffffff;
border-color: #dddddd;
background-color: #f3f3f3;
> .oo-ui-iconElement-icon,
> .oo-ui-indicatorElement-indicator {
opacity: 0.2;
}
}
}
}
.mw-widgets-stashedFileWidget-thumbnail-container {
cursor: default;
height: 5.5em;
text-align: left;
padding: 0;
background-color: #ffffff;
border: 1px solid #cccccc;
margin-bottom: 0.5em;
vertical-align: middle;
overflow: hidden;
border-radius: 2px;
.mw-widgets-stashedFileWidget-thumbnail {
height: 5.5em;
width: 5.5em;
position: absolute;
background-size: cover;
background-position: center center;
&.oo-ui-pendingElement-pending {
background-size: auto;
}
> .mw-widgets-stashedFileWidget-noThumbnail-icon {
opacity: 0.4;
background-color: #cccccc;
height: 5.5em;
width: 5.5em;
}
}
.mw-widgets-stashedFileWidget-info {
border: none;
background: none;
display: block;
height: 100%;
width: auto;
margin-left: 5.5em;
> .mw-widgets-stashedFileWidget-label {
position: relative;
> .mw-widgets-stashedFileWidget-fileName {
display: block;
float: none;
}
> .mw-widgets-stashedFileWidget-fileType {
display: block;
float: none;
}
}
}
}
.mw-widgets-stashedFileWidget-empty {
.mw-widgets-stashedFileWidget-thumbnail-container {
text-align: center;
.mw-widgets-stashedFileWidget-thumbnail,
.mw-widgets-stashedFileWidget-info {
margin: 0;
display: none;
}
}
.mw-widgets-stashedFileWidget-label {
color: #cccccc;
right: 0.5em;
}
&.oo-ui-indicatorElement {
.mw-widgets-stashedFileWidget-label {
right: 2em;
}
}
}

View file

@ -347,21 +347,7 @@
}
function finishUpload( moreData ) {
data = $.extend( data, moreData );
data.filekey = filekey;
data.action = 'upload';
data.format = 'json';
if ( !data.filename ) {
throw new Error( 'Filename not included in file data.' );
}
return api.postWithEditToken( data ).then( function ( result ) {
if ( result.upload && result.upload.warnings ) {
return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
}
return result;
} );
api.uploadFromStash( filekey, $.extend( data, moreData ) );
}
return this.upload( file, { stash: true, filename: data.filename } ).then(
@ -380,6 +366,29 @@
);
},
/**
* Finish an upload in the stash.
*
* @param {string} filekey
* @param {Object} data
*/
uploadFromStash: function ( filekey, data ) {
data.filekey = filekey;
data.action = 'upload';
data.format = 'json';
if ( !data.filename ) {
throw new Error( 'Filename not included in file data.' );
}
return this.postWithEditToken( data ).then( function ( result ) {
if ( result.upload && result.upload.warnings ) {
return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
}
return result;
} );
},
needToken: function () {
return true;
}

View file

@ -61,6 +61,7 @@
* @constructor
* @param {Object} config Configuration options
* @cfg {jQuery} [$overlay] Overlay to use for widgets in the booklet
* @cfg {string} [filekey] Sets the stashed file to finish uploading. Overrides most of the file selection process, and fetches a thumbnail from the server.
*/
mw.Upload.BookletLayout = function ( config ) {
// Parent constructor
@ -68,6 +69,8 @@
this.$overlay = config.$overlay;
this.filekey = config.filekey;
this.renderUploadForm();
this.renderInfoForm();
this.renderInsertForm();
@ -164,8 +167,13 @@
this.clear();
this.upload = this.createUpload();
this.setPage( 'upload' );
if ( this.filekey ) {
this.setFilekey( this.filekey );
}
return this.upload.getApi().then(
function ( api ) {
// If the user can't upload anything, don't give them the option to.
@ -217,10 +225,23 @@
layout = this,
file = this.getFile();
this.setFilename( file.name );
this.setPage( 'info' );
if ( this.filekey ) {
if ( file === null ) {
// Someone gonna get-a hurt real bad
throw new Error( 'filekey not passed into file select widget, which is impossible. Quitting while we\'re behind.' );
}
// Stashed file already uploaded.
deferred.resolve();
this.uploadPromise = deferred;
this.emit( 'fileUploaded' );
return deferred;
}
this.setFilename( file.name );
this.upload.setFile( file );
// The original file name might contain invalid characters, so use our sanitized one
this.upload.setFilename( this.getFilename() );
@ -402,14 +423,12 @@
var fieldset,
layout = this;
this.selectFileWidget = new OO.ui.SelectFileWidget( {
showDropTarget: true
} );
this.selectFileWidget = this.getFileWidget();
fieldset = new OO.ui.FieldsetLayout();
fieldset.addItems( [ this.selectFileWidget ] );
this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
// Validation
// Validation (if the SFW is for a stashed file, this never fires)
this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
this.selectFileWidget.on( 'change', function () {
@ -419,6 +438,23 @@
return this.uploadForm;
};
/**
* Gets the widget for displaying or inputting the file to upload.
*
* @return {OO.ui.SelectFileWidget|mw.widgets.StashedFileWidget}
*/
mw.Upload.BookletLayout.prototype.getFileWidget = function () {
if ( this.filekey ) {
return new mw.widgets.StashedFileWidget( {
filekey: this.filekey
} );
}
return new OO.ui.SelectFileWidget( {
showDropTarget: true
} );
};
/**
* Updates the file preview on the info form when a file is added.
*
@ -626,6 +662,20 @@
this.selectFileWidget.setValue( file );
};
/**
* Sets the filekey of a file already stashed on the server
* as the target of this upload operation.
*
* @protected
* @param {string} filekey
*/
mw.Upload.BookletLayout.prototype.setFilekey = function ( filekey ) {
this.upload.setFilekey( this.filekey );
this.selectFileWidget.setValue( filekey );
this.onUploadFormChange();
};
/**
* Clear the values of all fields
*

View file

@ -91,6 +91,20 @@
this.filename = filename;
};
/**
* Set the stashed file to finish uploading.
*
* @param {string} filekey
*/
UP.setFilekey = function ( filekey ) {
var upload = this;
this.setState( Upload.State.STASHED );
this.stashPromise = $.Deferred().resolve( function ( data ) {
return upload.api.uploadFromStash( filekey, data );
} );
};
/**
* Sets the filename based on the filename as it was on the upload.
*/