From 368ff0202fdca0bdccbe51ab2e32646a5fd3cdba Mon Sep 17 00:00:00 2001 From: Prateek Saxena Date: Mon, 3 Aug 2015 10:38:55 +0530 Subject: [PATCH] Add mw.Upload.Dialog as a UI to mw.Upload Change-Id: I5fcd0a5f8190134b87a75d3c22e7aefc619d3738 --- languages/i18n/en.json | 13 + languages/i18n/qqq.json | 13 + resources/Resources.php | 22 + .../src/mediawiki/mediawiki.Upload.Dialog.js | 521 ++++++++++++++++++ 4 files changed, 569 insertions(+) create mode 100644 resources/src/mediawiki/mediawiki.Upload.Dialog.js diff --git a/languages/i18n/en.json b/languages/i18n/en.json index c40625307d..aa71f3b326 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1398,6 +1398,19 @@ "upload-too-many-redirects": "The URL contained too many redirects", "upload-http-error": "An HTTP error occurred: $1", "upload-copy-upload-invalid-domain": "Copy uploads are not available from this domain.", + "upload-dialog-title": "Upload file", + "upload-dialog-error": "An error occurred", + "upload-dialog-warning": "A warning occurred", + "upload-dialog-button-cancel": "Cancel", + "upload-dialog-button-done": "Done", + "upload-dialog-button-save": "Save", + "upload-dialog-button-upload": "Upload", + "upload-dialog-label-select-file": "Select file", + "upload-dialog-label-infoform-title": "Details", + "upload-dialog-label-infoform-name": "Name", + "upload-dialog-label-infoform-description": "Description", + "upload-dialog-label-usage-title": "Usage", + "upload-dialog-label-usage-filename": "File name", "backend-fail-stream": "Could not stream file \"$1\".", "backend-fail-backup": "Could not backup file \"$1\".", "backend-fail-notexists": "The file $1 does not exist.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 17f1c46a92..1a7b182780 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1569,6 +1569,19 @@ "upload-too-many-redirects": "Error message shown when uploading a file via URL, if the upload failed because the URL returned too many redirects.", "upload-http-error": "Parameters:\n* $1 - error message", "upload-copy-upload-invalid-domain": "Error message shown if a user is trying to upload (i.e. copy) a file from a website that is not in $wgCopyUploadsDomains (if set).\n\nSee also:\n* {{msg-mw|http-invalid-url}}\n* {{msg-mw|tmp-create-error}}\n* {{msg-mw|tmp-write-error}}", + "upload-dialog-title": "Title of the upload dialog box", + "upload-dialog-error": "Error message from upload", + "upload-dialog-warning": "Warning message from upload", + "upload-dialog-button-cancel": "Button to cancel the dialog", + "upload-dialog-button-done": "Button to close the dialog once upload is complete", + "upload-dialog-button-save": "Button to save the file", + "upload-dialog-button-upload": "Button to initiate upload", + "upload-dialog-label-select-file": "Label for the select file widget", + "upload-dialog-label-infoform-title": "Title for the information form", + "upload-dialog-label-infoform-name": "Label for the file name input", + "upload-dialog-label-infoform-description": "Label for the file description input", + "upload-dialog-label-usage-title": "Title for the usage form", + "upload-dialog-label-usage-filename": "Label for the file name input", "backend-fail-stream": "Parameters:\n* $1 - a filename", "backend-fail-backup": "Parameters:\n* $1 - a filename", "backend-fail-notexists": "Parameters:\n* $1 - a filename", diff --git a/resources/Resources.php b/resources/Resources.php index 2396128849..c91803b8bf 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1107,6 +1107,28 @@ return array( 'mediawiki.api.upload', ), ), + 'mediawiki.Upload.Dialog' => array( + 'scripts' => 'resources/src/mediawiki/mediawiki.Upload.Dialog.js', + 'dependencies' => array( + 'oojs-ui', + 'mediawiki.Upload', + ), + 'messages' => array( + 'upload-dialog-title', + 'upload-dialog-error', + 'upload-dialog-warning', + 'upload-dialog-button-cancel', + 'upload-dialog-button-done', + 'upload-dialog-button-save', + 'upload-dialog-button-upload', + 'upload-dialog-label-select-file', + 'upload-dialog-label-infoform-title', + 'upload-dialog-label-infoform-name', + 'upload-dialog-label-infoform-description', + 'upload-dialog-label-usage-title', + 'upload-dialog-label-usage-filename', + ), + ), 'mediawiki.toc' => array( 'scripts' => 'resources/src/mediawiki/mediawiki.toc.js', 'dependencies' => 'mediawiki.cookie', diff --git a/resources/src/mediawiki/mediawiki.Upload.Dialog.js b/resources/src/mediawiki/mediawiki.Upload.Dialog.js new file mode 100644 index 0000000000..c47dd5587a --- /dev/null +++ b/resources/src/mediawiki/mediawiki.Upload.Dialog.js @@ -0,0 +1,521 @@ +( function ( $, mw ) { + + /** + * mw.Upload.Dialog encapsulates the process of uploading a file + * to MediaWiki using the {@link mw.Upload mw.Upload} model. + * The dialog emits events that can be used to get the stashed + * upload and the final file. It can be extended to accept + * additional fields from the user for specific scenarios like + * for Commons, or campaigns. + * + * ## Structure + * + * The {@link OO.ui.ProcessDialog dialog} has three steps- + * + * - **Upload**: Has a {@link OO.ui.SelectFileWidget field} to get the + * file object. + * + * - **Information**: Has a {@link OO.ui.FormLayout form} to + * collect metadata. This can be extended. + * + * - **Insert**: Has details on how to use the file that was uploaded. + * + * Each step has a form associated with it defined in + * {@link mw.Upload.Dialog#renderUploadForm renderUploadForm}, + * {@link mw.Upload.Dialog#renderInfoForm renderInfoForm}, and + * {@link mw.Upload.Dialog#renderInsertForm renderInfoForm}. The + * {@link mw.Upload.Dialog#getFile getFile}, + * {@link mw.Upload.Dialog#getFilename getFilename}, and + * {@link mw.Upload.Dialog#getText getText} methods are used to get + * the information filled in these forms, required to call + * {@link mw.Upload mw.Upload}. + * + * ## Usage + * + * To use, setup a {@link OO.ui.WindowManager window manager} like for normal + * dialogs- + * + * var uploadDialog = new mw.Upload.Dialog( { size: 'small' } ); + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * windowManager.addWindows( [ uploadDialog ] ); + * windowManager.openWindow( uploadDialog ); + * + * The dialog's closing promise, + * {@link mw.Upload.Dialog#event-fileUploaded fileUploaded}, + * and {@link mw.Upload.Dialog#event-fileSaved fileSaved} events can + * be used to get details of the upload + * + * ## Extending + * + * To extend using {@link mw.Upload mw.Upload}, override + * {@link mw.Upload.Dialog#renderInfoForm renderInfoForm} to render + * the form required for the specific use-case. Update the + * {@link mw.Upload.Dialog#getFilename getFilename}, and + * {@link mw.Upload.Dialog#getText getText} methods to return data + * from your newly created form. If you added new fields you'll also have + * to update the {@link #getTeardownProcess} method. + * + * If you plan to use a different upload model, apart from what is mentioned + * above, you'll also have to override the + * {@link mw.Upload.Dialog#getUploadObject getUploadObject} method to + * return the new model. The {@link mw.Upload.Dialog#saveFile saveFile}, and + * the {@link mw.Upload.Dialog#uploadFile uploadFile} methods need to be + * overriden to use the new model and data returned from the forms. + * + * @class mw.Upload.Dialog + * @uses mw.Upload + * @extends OO.ui.ProcessDialog + */ + mw.Upload.Dialog = function ( config ) { + // Parent constructor + mw.Upload.Dialog.parent.call( this, config ); + }; + + /* Setup */ + + OO.inheritClass( mw.Upload.Dialog, OO.ui.ProcessDialog ); + + /* Static Properties */ + + /** + * @inheritdoc + * @property title + */ + /*jshint -W024*/ + mw.Upload.Dialog.static.title = mw.msg( 'upload-dialog-title' ); + + /** + * @inheritdoc + * @property actions + */ + mw.Upload.Dialog.static.actions = [ + { + flags: 'safe', + action: 'cancel', + label: mw.msg( 'upload-dialog-button-cancel' ), + modes: [ 'upload', 'insert', 'save' ] + }, + { + flags: [ 'primary', 'progressive' ], + label: mw.msg( 'upload-dialog-button-done' ), + action: 'insert', + modes: 'insert' + }, + { + flags: [ 'primary', 'constructive' ], + label: mw.msg( 'upload-dialog-button-save' ), + action: 'save', + modes: 'save' + }, + { + flags: [ 'primary', 'progressive' ], + label: mw.msg( 'upload-dialog-button-upload' ), + action: 'upload', + modes: 'upload' + } + ]; + /*jshint +W024*/ + + /* Properties */ + + /** + * @property {OO.ui.FormLayout} uploadForm + * The form rendered in the first step to get the file object. + * Rendered in {@link mw.Upload.Dialog#renderUploadForm renderUploadForm}. + */ + + /** + * @property {OO.ui.FormLayout} infoForm + * The form rendered in the second step to get metadata. + * Rendered in {@link mw.Upload.Dialog#renderInfoForm renderInfoForm} + */ + + /** + * @property {OO.ui.FormLayout} insertForm + * The form rendered in the third step to show usage + * Rendered in {@link mw.Upload.Dialog#renderInsertForm renderInsertForm} + */ + + /* Events */ + + /** + * A `fileUploaded` event is emitted from the + * {@link mw.Upload.Dialog#uploadFile uploadFile} method. + * + * @event fileUploaded + */ + + /** + * A `fileSaved` event is emitted from the + * {@link mw.Upload.Dialog#saveFile saveFile} method. + * + * @event fileSaved + */ + + /* Methods */ + + /** + * @inheritdoc + */ + mw.Upload.Dialog.prototype.initialize = function () { + mw.Upload.Dialog.parent.prototype.initialize.call( this ); + + this.renderUploadForm(); + this.renderInfoForm(); + this.renderInsertForm(); + + this.uploadFormPanel = new OO.ui.PanelLayout( { + scrollable: true, + padded: true, + content: [ this.uploadForm ] + } ); + this.infoFormPanel = new OO.ui.PanelLayout( { + scrollable: true, + padded: true, + content: [ this.infoForm ] + } ); + this.insertFormPanel = new OO.ui.PanelLayout( { + scrollable: true, + padded: true, + content: [ this.insertForm ] + } ); + + this.panels = new OO.ui.StackLayout(); + this.panels.addItems( [ + this.uploadFormPanel, + this.infoFormPanel, + this.insertFormPanel + ] ); + + this.$body.append( this.panels.$element ); + }; + + /** + * @inheritdoc + */ + mw.Upload.Dialog.prototype.getBodyHeight = function () { + return 300; + }; + + /** + * Switch between the panels. + * + * @param {string} panel Panel name: 'upload', 'info', 'insert' + */ + mw.Upload.Dialog.prototype.switchPanels = function ( panel ) { + switch ( panel ) { + case 'upload': + this.panels.setItem( this.uploadFormPanel ); + this.actions.setMode( 'upload' ); + break; + case 'info': + this.panels.setItem( this.infoFormPanel ); + this.actions.setMode( 'save' ); + break; + case 'insert': + this.panels.setItem( this.insertFormPanel ); + this.actions.setMode( 'insert' ); + break; + } + }; + + /** + * @inheritdoc + */ + mw.Upload.Dialog.prototype.getSetupProcess = function ( data ) { + return mw.Upload.Dialog.parent.prototype.getSetupProcess.call( this, data ) + .next( function () { + this.upload = this.getUploadObject(); + this.switchPanels( 'upload' ); + this.actions.setAbilities( { upload: false } ); + }, this ); + }; + + /** + * @inheritdoc + */ + mw.Upload.Dialog.prototype.getActionProcess = function ( action ) { + var dialog = this; + + if ( action === 'upload' ) { + return new OO.ui.Process( function () { + dialog.filenameWidget.setValue( dialog.getFile().name ); + dialog.switchPanels( 'info' ); + dialog.actions.setAbilities( { save: false } ); + return dialog.uploadFile(); + } ); + } + if ( action === 'save' ) { + return new OO.ui.Process( dialog.saveFile() ); + } + if ( action === 'insert' ) { + return new OO.ui.Process( function () { + dialog.close( dialog.upload ); + } ); + } + if ( action === 'cancel' ) { + return new OO.ui.Process( dialog.close() ); + } + + return mw.Upload.Dialog.parent.prototype.getActionProcess.call( this, action ); + }; + + /** + * @inheritdoc + */ + mw.Upload.Dialog.prototype.getTeardownProcess = function ( data ) { + return mw.Upload.Dialog.parent.prototype.getTeardownProcess.call( this, data ) + .next( function () { + // Clear the values of all fields + this.selectFileWidget.setValue( null ); + this.filenameWidget.setValue( null ).setValidityFlag( true ); + this.descriptionWidget.setValue( null ).setValidityFlag( true ); + this.filenameUsageWidget.setValue( null ); + }, this ); + }; + + /* Uploading */ + + /** + * Get the upload model object required for this dialog. Can be + * extended to different models. + * + * @return {mw.Upload} + */ + mw.Upload.Dialog.prototype.getUploadObject = function () { + return new mw.Upload(); + }; + + /** + * Uploads the file that was added in the upload form. Uses + * {@link mw.Upload.Dialog#getFile getFile} to get the HTML5 + * file object. + * + * @protected + * @fires fileUploaded + * @return {jQuery.Promise} + */ + mw.Upload.Dialog.prototype.uploadFile = function () { + var dialog = this, + file = this.getFile(); + this.upload.setFile( file ); + this.uploadPromise = this.upload.uploadToStash(); + this.uploadPromise.then( function () { + dialog.emit( 'fileUploaded' ); + } ); + + return this.uploadPromise; + }; + + /** + * Saves the stash finalizes upload. Uses + * {@link mw.Upload.Dialog#getFilename getFilename}, and + * {@link mw.Upload.Dialog#getText getText} to get details from + * the form. + * + * @protected + * @fires fileSaved + * @returns {jQuery.Promise} Rejects the promise with an + * {@link OO.ui.Error error}, or resolves if the upload was successful. + */ + mw.Upload.Dialog.prototype.saveFile = function () { + var dialog = this, + promise = $.Deferred(); + + this.upload.setFilename( this.getFilename() ); + this.upload.setText( this.getText() ); + + this.uploadPromise.always( function () { + + if ( dialog.upload.getState() === mw.Upload.State.ERROR ) { + promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-error' ) ) ); + return false; + } + + if ( dialog.upload.getState() === mw.Upload.State.WARNING ) { + promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-error' ) ) ); + return false; + } + + dialog.upload.finishStashUpload().then( function () { + var name; + + if ( dialog.upload.getState() === mw.Upload.State.ERROR ) { + promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-error' ) ) ); + return false; + } + + if ( dialog.upload.getState() === mw.Upload.State.WARNING ) { + promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-warning' ) ) ); + return false; + } + + // Normalize page name and localise the 'File:' prefix + name = new mw.Title( 'File:' + dialog.upload.getFilename() ).toString(); + dialog.filenameUsageWidget.setValue( '[[' + name + ']]' ); + dialog.switchPanels( 'insert' ); + + promise.resolve(); + dialog.emit( 'fileSaved' ); + } ); + } ); + + return promise.promise(); + }; + + /* Form renderers */ + + /** + * Renders and returns the upload form and sets the + * {@link mw.Upload.Dialog#uploadForm uploadForm} property. + * Validates the form and + * {@link OO.ui.ActionSet#setAbilities sets abilities} + * for the dialog accordingly. + * + * @protected + * @returns {OO.ui.FormLayout} + */ + mw.Upload.Dialog.prototype.renderUploadForm = function () { + var fieldset, + dialog = this; + + this.selectFileWidget = new OO.ui.SelectFileWidget(); + fieldset = new OO.ui.FieldsetLayout( { label: mw.msg( 'upload-dialog-label-select-file' ) } ); + fieldset.addItems( [ this.selectFileWidget ] ); + this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } ); + + // Validation + this.selectFileWidget.on( 'change', function ( value ) { + dialog.actions.setAbilities( { upload: !!value } ); + } ); + + return this.uploadForm; + }; + + /** + * Renders and returns the information form for collecting + * metadata and sets the {@link mw.Upload.Dialog#infoForm infoForm} + * property. + * Validates the form and + * {@link OO.ui.ActionSet#setAbilities sets abilities} + * for the dialog accordingly. + * + * @protected + * @returns {OO.ui.FormLayout} + */ + mw.Upload.Dialog.prototype.renderInfoForm = function () { + var fieldset, + dialog = this; + + this.filenameWidget = new OO.ui.TextInputWidget( { + indicator: 'required', + required: true, + validate: /.+/ + } ); + this.descriptionWidget = new OO.ui.TextInputWidget( { + indicator: 'required', + required: true, + validate: /.+/, + multiline: true, + autosize: true + } ); + + fieldset = new OO.ui.FieldsetLayout( { + label: mw.msg( 'upload-dialog-label-infoform-title' ) + } ); + fieldset.addItems( [ + new OO.ui.FieldLayout( this.filenameWidget, { + label: mw.msg( 'upload-dialog-label-infoform-name' ), + align: 'top' + } ), + new OO.ui.FieldLayout( this.descriptionWidget, { + label: mw.msg( 'upload-dialog-label-infoform-description' ), + align: 'top' + } ) + ] ); + this.infoForm = new OO.ui.FormLayout( { items: [ fieldset ] } ); + + // Validation + function checkValidity() { + var validityPromises = [ + dialog.filenameWidget.isValid(), + dialog.descriptionWidget.isValid() + ]; + + $.when.apply( $, validityPromises ).done( function () { + var allValid, + values = Array.prototype.slice.apply( arguments ); + allValid = values.every( function ( value ) { + return value; + } ); + + dialog.actions.setAbilities( { save: allValid } ); + } ); + } + this.filenameWidget.on( 'change', checkValidity ); + this.descriptionWidget.on( 'change', checkValidity ); + + return this.infoForm; + }; + + /** + * Renders and returns the insert form to show file usage and + * sets the {@link mw.Upload.Dialog#insertForm insertForm} property. + * + * @protected + * @returns {OO.ui.FormLayout} + */ + mw.Upload.Dialog.prototype.renderInsertForm = function () { + var fieldset; + + this.filenameUsageWidget = new OO.ui.TextInputWidget(); + fieldset = new OO.ui.FieldsetLayout( { + label: mw.msg( 'upload-dialog-label-usage-title' ) + } ); + fieldset.addItems( [ + new OO.ui.FieldLayout( this.filenameUsageWidget, { + label: mw.msg( 'upload-dialog-label-usage-filename' ), + align: 'top' + } ) + ] ); + this.insertForm = new OO.ui.FormLayout( { items: [ fieldset ] } ); + + return this.insertForm; + }; + + /* Getters */ + + /** + * Gets the file object from the + * {@link mw.Upload.Dialog#uploadForm upload form}. + * + * @protected + * @returns {File|null} + */ + mw.Upload.Dialog.prototype.getFile = function () { + return this.selectFileWidget.getValue(); + }; + + /** + * Gets the file name from the + * {@link mw.Upload.Dialog#infoForm information form}. + * + * @protected + * @returns {string} + */ + mw.Upload.Dialog.prototype.getFilename = function () { + return this.filenameWidget.getValue(); + }; + + /** + * Gets the page text from the + * {@link mw.Upload.Dialog#infoForm information form}. + * + * @protected + * @returns {string} + */ + mw.Upload.Dialog.prototype.getText = function () { + return this.descriptionWidget.getValue(); + }; +}( jQuery, mediaWiki ) ); -- 2.20.1