Matthias Mullie has uploaded a new change for review.
https://gerrit.wikimedia.org/r/206827
Change subject: Refactor flowEventsMixinApiRequestInteractiveHandler
......................................................................
Refactor flowEventsMixinApiRequestInteractiveHandler
That method was (and still is) a huge monster :)
It was hard to consistently do things in there (e.g. error
handling or post-processing cleaning-up)
I've made it a chain of promises. The biggest annoyance is
that the handlers don't return data in a way that would
allow them to easily be chained, so all of those have a
small wrapper around them to re-format the data. That way,
this refactor didn't have to touch api(Pre)Handlers code.
I think this drastically improved to understand what's
happening where.
And now the .flow-api-inprogress is only removed after
*everything* has run, including the apiHandlers.
Bug: T96811
Change-Id: Ibb9a6e846437cbc8ed9b851b9d4ed45f00f8f90d
---
M modules/engine/components/common/flow-component-events.js
M modules/engine/misc/flow-api.js
2 files changed, 136 insertions(+), 123 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow
refs/changes/27/206827/1
diff --git a/modules/engine/components/common/flow-component-events.js
b/modules/engine/components/common/flow-component-events.js
index 226fbd4..6f629e5 100644
--- a/modules/engine/components/common/flow-component-events.js
+++ b/modules/engine/components/common/flow-component-events.js
@@ -226,8 +226,8 @@
/**
* Utility to get error message for API result.
*
- * @param string code
- * @param Object result
+ * @param {string} code
+ * @param {Object} result
* @returns string
*/
function flowGetApiErrorMessage( code, result ) {
@@ -255,11 +255,9 @@
* @returns {$.Promise}
*/
function flowEventsMixinApiRequestInteractiveHandler( event ) {
- var $deferred,
- $handlerDeferred,
- handlerPromises = [],
+ var $deferred = $.Deferred(),
+ deferreds = [$deferred],
$target,
- preHandlerReturn,
self = event.currentTarget || event.delegateTarget ||
event.target,
$this = $( self ),
flowComponent = mw.flow.getPrototypeMethod(
'component', 'getInstanceByElement' )( $this ),
@@ -271,7 +269,8 @@
component: flowComponent
},
args = Array.prototype.slice.call( arguments, 0 ),
- queryMap = flowComponent.Api.getQueryMap( self.href ||
self );
+ queryMap = flowComponent.Api.getQueryMap( self.href ||
self ),
+ preHandlers = [];
event.preventDefault();
@@ -292,136 +291,107 @@
// Make sure an API call is not already in progress for this
target
if ( $target.closest( '.flow-api-inprogress' ).length ) {
- flowComponent.debug( false, 'apiRequest already in
progress', arguments );
- return $.Deferred().reject().promise();
+ $deferred.reject( 'fail-api-inprogress', { error: {
info: 'apiRequest already in progress' } } );
+ } else {
+ $deferred.resolve( args );
}
- // Mark the target node as "in progress" to disallow any
further API calls until it finishes
- $target.addClass( 'flow-api-inprogress' );
- $this.addClass( 'flow-api-inprogress' );
+ $deferred.then( function ( args ) {
+ // Mark the target node as "in progress" to disallow
any further API calls until it finishes
+ $target.addClass( 'flow-api-inprogress' );
+ $this.addClass( 'flow-api-inprogress' );
- // Let generic pre-handler take care of edit conflicts
- $.each( flowComponent.UI.events.globalApiPreHandlers, function(
key, callbackArray ) {
- $.each( callbackArray, function ( i, callbackFn ) {
- queryMap = callbackFn.apply( self, args );
- } );
+ // Remove existing errors from previous attempts
+ flowComponent.emitWithReturn( 'removeError', $this );
} );
- // We'll return a deferred object that won't resolve before
apiHandlers
- // are resolved
- $handlerDeferred = $.Deferred();
+ // chain apiPreHandler callbacks
+ preHandlers = _getApiPreHandlers( self, handlerName );
+ $.each( preHandlers, function ( i, callback ) {
+ $deferred = $deferred.then( callback );
+ } );
- // Use the pre-callback to find out if we should process this
- if ( flowComponent.UI.events.apiPreHandlers[ handlerName ] ) {
- // apiPreHandlers can return FALSE to prevent
processing,
- // nothing at all to proceed,
- // or new queryMap (= API params)
- $.each( flowComponent.UI.events.apiPreHandlers[
handlerName ], function ( i, callbackFn ) {
- // make sure queryMap is up to date (may have
been altered by
- // previous preHandler)
- args.splice( 2, 1, queryMap );
- preHandlerReturn = callbackFn.apply( self, args
);
+ // execute API call
+ $deferred = $deferred.then( function ( args ) {
+ var queryMap = args[2];
+ return flowComponent.Api.requestFromNode( self,
queryMap );
+ } );
- if ( preHandlerReturn === false ) {
- // Callback returned false; break out
of this loop
- return false;
- } else {
- queryMap = preHandlerReturn;
+ // alter API response: apiHandler expects a 1st param info (that
+ // includes 'status') & `this` being the target element
+ $deferred = $deferred.then(
+ // success
+ function () {
+ var args = Array.prototype.slice.call(
arguments, 0 );
+ info.status = 'done';
+ args.unshift( info );
+ return $.Deferred().resolveWith( self, args );
+ },
+ // failure
+ function ( code, result ) {
+ var errorMsg,
+ args = Array.prototype.slice.call(
arguments, 0 ),
+ $form = $this.closest( 'form' );
+
+ info.status = 'fail';
+ args.unshift( info );
+
+ /*
+ * In the event of edit conflicts, store the
previous
+ * revision id so we can re-submit an edit
against the
+ * current id later.
+ */
+ if ( result.error && result.error.prev_revision
) {
+ $form.data( 'flow-prev-revision',
result.error.prev_revision.revision_id );
}
- } );
- if ( preHandlerReturn === false ) {
- // Last callback returned false
- flowComponent.debug( false, 'apiPreHandler
returned false', handlerName, args );
+ /*
+ * Generic error handling: displays error
message in the
+ * nearest error container.
+ *
+ * Errors returned by MW/Flow should always be
in the
+ * same format. If the request failed without a
specific
+ * error message, just fall back to some
default error.
+ */
+ errorMsg =
flowComponent.constructor.static.getApiErrorMessage( code, result );
+ flowComponent.debug( false, errorMsg,
handlerName, args );
+ flowComponent.emitWithReturn( 'showError',
$this, errorMsg );
- // Abort any old request in flight; this is
normally done automatically by requestFromNode
flowComponent.Api.abortOldRequestFromNode(
self, queryMap, null );
- // @todo support for multiple indicators on
same target
- $target.removeClass( 'flow-api-inprogress' );
- $this.removeClass( 'flow-api-inprogress' );
-
- return $.Deferred().reject().promise();
+ // keep going & process those apiHandlers;
based on info.status,
+ // they'll know if they're dealing with
successful submissions,
+ // or cleaning up after error
+ return $.Deferred().rejectWith( self, args );
}
+ );
+
+ // chain apiHandler callbacks: run apiHandlers in both success
& failure
+ // cases, it can distinguish in how it needs to wrap up
depending on
+ // info.status
+ if ( flowComponent.UI.events.apiHandlers[ handlerName ] ) {
+ $.each( flowComponent.UI.events.apiHandlers[
handlerName ], function ( i, callback ) {
+ /*
+ * apiHandlers will return promises that won't
resolve until
+ * the apiHandler has completed all it needs to
do.
+ * These handlers aren't chainable, though
(although we only
+ * have 1 per call, AFAIK), they don't return
the same data the
+ * next handler assumes.
+ * In order to suspend something until all of
these apiHandlers
+ * have completed, we'll combine them in an
array which we can
+ * keep tabs on until all of these promises are
done ($.when)
+ */
+ deferreds.push( $deferred.then( callback,
callback ) );
+ } );
}
- // Make the request
- $deferred = flowComponent.Api.requestFromNode( self, queryMap );
- if ( !$deferred ) {
- mw.flow.debug( '[FlowApi] [interactiveHandlers]
apiRequest element is not anchor or form element' );
- $deferred = $.Deferred();
- $deferred.rejectWith( { error: { info: 'Not an anchor
or form' } } );
- }
-
- // Remove the load indicator
- $deferred.always( function () {
- // @todo support for multiple indicators on same target
+ // cleanup
+ $.when.apply( $, deferreds ).always( function() {
$target.removeClass( 'flow-api-inprogress' );
$this.removeClass( 'flow-api-inprogress' );
} );
- // Remove existing errors from previous attempts
- flowComponent.emitWithReturn( 'removeError', $this );
-
- // We'll return a deferred object that won't resolve before
apiHandlers
- // are resolved
- $handlerDeferred = $.Deferred();
-
- // If this has a special api handler, bind it to the callback.
- if ( flowComponent.UI.events.apiHandlers[ handlerName ] ) {
- $deferred
- .done( function () {
- var args = Array.prototype.slice.call(
arguments, 0 );
- info.status = 'done';
- args.unshift( info );
- $.each(
flowComponent.UI.events.apiHandlers[ handlerName ], function ( i, callbackFn ) {
- handlerPromises.push(
callbackFn.apply( self, args ) );
- } );
- } )
- .fail( function ( code, result ) {
- var errorMsg,
- args =
Array.prototype.slice.call( arguments, 0 ),
- $form = $this.closest( 'form' );
-
- info.status = 'fail';
- args.unshift( info );
-
- /*
- * In the event of edit conflicts,
store the previous
- * revision id so we can re-submit an
edit against the
- * current id later.
- */
- if ( result.error &&
result.error.prev_revision ) {
- $form.data(
'flow-prev-revision', result.error.prev_revision.revision_id );
- }
-
- /*
- * Generic error handling: displays
error message in the
- * nearest error container.
- *
- * Errors returned by MW/Flow should
always be in the
- * same format. If the request failed
without a specific
- * error message, just fall back to
some default error.
- */
- errorMsg =
flowComponent.constructor.static.getApiErrorMessage( code, result );
- flowComponent.emitWithReturn(
'showError', $this, errorMsg );
-
- $.each(
flowComponent.UI.events.apiHandlers[ handlerName ], function ( i, callbackFn ) {
- handlerPromises.push(
callbackFn.apply( self, args ) );
- } );
- } )
- .always( function() {
- // Resolve/reject the promised
deferreds when all apiHandler
- // deferreds have been resolved/rejected
- $.when.apply( $, handlerPromises )
- .done( $handlerDeferred.resolve
)
- .fail( $handlerDeferred.reject
);
- } );
- }
-
- // Return an aggregate promise that resolves when all are
resolved, or
- // rejects once one of them is rejected
- return $handlerDeferred.promise();
+ return $deferred.promise();
}
FlowComponentEventsMixin.UI.events.interactiveHandlers.apiRequest =
flowEventsMixinApiRequestInteractiveHandler;
@@ -902,6 +872,49 @@
return $result;
}
+ /**
+ * @param {Element} target
+ * @param {String} handlerName
+ * @returns {Array<function>}
+ * @private
+ */
+ function _getApiPreHandlers( target, handlerName ) {
+ var flowComponent = mw.flow.getPrototypeMethod( 'component',
'getInstanceByElement' )( $( target ) ),
+ preHandlers = [];
+
+ // Compile a list of all preHandlers to be run
+ $.each( flowComponent.UI.events.globalApiPreHandlers, function(
key, callbackArray ) {
+ preHandlers.concat( callbackArray );
+ } );
+ if ( flowComponent.UI.events.apiPreHandlers[ handlerName ] ) {
+ preHandlers = flowComponent.UI.events.apiPreHandlers[
handlerName ];
+ }
+
+ $.each( preHandlers, function ( key, callback ) {
+ /**
+ * apiPreHandlers aren't properly set up to serve as
chained promise
+ * callbacks (they'll return false instead of returning
a rejected
+ * promise, the incoming & outgoing params don't line
up)
+ * This will wrap all those callbacks into callbacks we
can chain.
+ *
+ * @param {object} args
+ * @returns {object}
+ */
+ preHandlers[key] = function ( args ) {
+ var queryMap = callback.apply( target, args );
+ if ( queryMap === false ) {
+ return $.Deferred().reject(
'fail-prehandler', { error: { info: 'apiPreHandler returned false' } } );
+ }
+
+ // update changed queryMap
+ args.splice( 2, 0, queryMap );
+ return args;
+ };
+ } );
+
+ return preHandlers;
+ }
+
// Copy static and prototype from mixin to main class
mw.flow.mixinComponent( 'component', FlowComponentEventsMixin );
}( jQuery, mediaWiki ) );
diff --git a/modules/engine/misc/flow-api.js b/modules/engine/misc/flow-api.js
index 1949950..5b9af60 100644
--- a/modules/engine/misc/flow-api.js
+++ b/modules/engine/misc/flow-api.js
@@ -35,7 +35,7 @@
/**
* Makes the actual API call and returns
* @param {Object|String} [params] May be a JSON object string
- * @param {String} [pageName]
+ * @param {String} [method]
* @returns {$.Promise}
*/
function flowApiCall( params, method ) {
@@ -60,11 +60,11 @@
if ( !params.action ) {
mw.flow.debug( '[FlowApi] apiCall error:
missing action string', arguments );
- return $deferred.rejectWith({ error: 'Invalid
action' }).promise();
+ return $deferred.reject( 'fail-apirequest', {
error: { info: 'Invalid action' } } ).promise();
}
if ( !params.page ) {
mw.flow.debug( '[FlowApi] apiCall error:
missing page string', [ mw.config.get( 'wgPageName' ) ], arguments );
- return $deferred.rejectWith({ error: 'Invalid
title' } ).promise();
+ return $deferred.reject( 'fail-apirequest', {
error: { info: 'Invalid title' } } ).promise();
}
if ( method === 'POST' ) {
@@ -78,7 +78,7 @@
return mwApi.postWithToken( tokenType, params );
} else if ( method !== 'GET' ) {
- return $deferred.rejectWith({ error: 'Unknown
submission method: ' + method }).promise();
+ return $deferred.reject( 'fail-apirequest', {
error: { info: 'Unknown submission method: ' + method } } ).promise();
} else {
return mwApi.get( params );
}
@@ -290,7 +290,7 @@
} else if ( $node.is( 'form, input, button, textarea, select,
option' ) ) {
return this.requestFromForm.apply( this, arguments );
} else {
- return $.Deferred().reject();
+ return $.Deferred().reject( 'fail-apirequest', { error:
{ info: 'apiRequest element is not anchor or form element' } } );
}
}
--
To view, visit https://gerrit.wikimedia.org/r/206827
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ibb9a6e846437cbc8ed9b851b9d4ed45f00f8f90d
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits